chore: remove dead code from leptos_dom (now found in tachys)

This commit is contained in:
Greg Johnston 2024-07-31 21:00:55 -04:00
parent 3a4ad07a91
commit 0e979a0767
25 changed files with 0 additions and 10478 deletions

View file

@ -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<OnceCell<()>>,
#[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<View>,
closing: Comment,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) id: Option<HydrationKey>,
#[cfg(debug_assertions)]
pub(crate) view_marker: Option<String>,
}
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("<Component />");
} else {
#[cfg(debug_assertions)]
writeln!(f, "<{}>", self.name)?;
#[cfg(not(debug_assertions))]
f.write_str("<Component>")?;
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("</Component>")?;
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::<web_sys::Node>()
.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<ComponentRepr> 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 = "<Component />", 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<Oco<'static, str>>) -> 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<Oco<'static, str>>,
id: Option<HydrationKey>,
) -> Self {
Self::new_with_id_concrete(name.into(), id)
}
fn new_with_id_concrete(
name: Oco<'static, str>,
id: Option<HydrationKey>,
) -> Self {
let markers = (
Comment::new(format!("</{name}>"), &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<F, V>
where
F: FnOnce() -> V,
V: IntoView,
{
id: Option<HydrationKey>,
name: Oco<'static, str>,
children_fn: F,
}
impl<F, V> Component<F, V>
where
F: FnOnce() -> V,
V: IntoView,
{
/// Creates a new component.
pub fn new(name: impl Into<Oco<'static, str>>, f: F) -> Self {
Self {
id: HydrationCtx::id(),
name: name.into(),
children_fn: f,
}
}
}
impl<F, V> IntoView for Component<F, V>
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()
}
}

View file

@ -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<RefCell<Box<Option<View>>>>,
closing: Comment,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) id: Option<HydrationKey>,
}
impl fmt::Debug for DynChildRepr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use fmt::Write;
f.write_str("<DynChild>\n")?;
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
writeln!(
pad_adapter,
"{:#?}",
self.child.borrow().deref().deref().as_ref().unwrap()
)?;
f.write_str("</DynChild>")
}
}
#[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<HydrationKey>) -> Self {
let markers = (
Comment::new("</DynChild>", &id, true),
#[cfg(debug_assertions)]
Comment::new("<DynChild>", &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<CF, N>
where
CF: Fn() -> N + 'static,
N: IntoView,
{
id: Option<HydrationKey>,
child_fn: CF,
}
impl<CF, N> DynChild<CF, N>
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<HydrationKey>, child_fn: CF) -> Self {
Self { id, child_fn }
}
}
impl<CF, N> IntoView for DynChild<CF, N>
where
CF: Fn() -> N + 'static,
N: IntoView,
{
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", name = "<DynChild />", skip_all)
)]
#[inline]
fn into_view(self) -> View {
// concrete inner function
#[inline(never)]
fn create_dyn_view(
component: DynChildRepr,
child_fn: Box<dyn Fn() -> 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<Option<web_sys::Node>>| {
#[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::<web_sys::Text>()
.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::<web_sys::Element>()
.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::<web_sys::Text>();
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<Node>;
fn previous_non_view_marker_sibling(&self) -> Option<Node>;
}
impl NonViewMarkerSibling for web_sys::Node {
#[cfg_attr(not(debug_assertions), inline(always))]
fn next_non_view_marker_sibling(&self) -> Option<Node> {
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<Node> {
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()
}
}
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -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<ErrorKey, Error>);
/// 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<T> From<T> for ErrorKey
where
T: Into<Cow<'static, str>>,
{
#[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<ErrorKey, Error>);
impl Iterator for IntoIter {
type Item = (ErrorKey, Error);
#[inline(always)]
fn next(
&mut self,
) -> std::option::Option<<Self as std::iter::Iterator>::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<<Self as std::iter::Iterator>::Item> {
self.0.next()
}
}
impl<T, E> IntoView for Result<T, E>
where
T: IntoView + 'static,
E: Into<Error>,
{
fn into_view(self) -> crate::View {
let id = ErrorKey(
HydrationCtx::peek()
.map(|n| n.to_string())
.unwrap_or_default()
.into(),
);
let errors = use_context::<RwSignal<Errors>>();
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 `<ErrorBoundary/>`
pub fn insert<E>(&mut self, key: ErrorKey, error: E)
where
E: Into<Error>,
{
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<E>(&mut self, error: E)
where
E: Into<Error>,
{
self.0.insert(Default::default(), error.into());
}
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
pub fn remove(&mut self, key: &ErrorKey) -> Option<Error> {
self.0.remove(key)
}
/// An iterator over all the errors, in arbitrary order.
#[inline(always)]
pub fn iter(&self) -> Iter<'_> {
Iter(self.0.iter())
}
}

View file

@ -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<I, V> IntoFragment for I
where
I: IntoIterator<Item = V>,
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<HydrationKey>,
/// The nodes contained in the fragment.
pub nodes: Vec<View>,
#[cfg(debug_assertions)]
pub(crate) view_marker: Option<String>,
}
impl FromIterator<View> for Fragment {
fn from_iter<T: IntoIterator<Item = View>>(iter: T) -> Self {
Fragment::new(iter.into_iter().collect())
}
}
impl From<View> for Fragment {
fn from(view: View) -> Self {
Fragment::new(vec![view])
}
}
impl From<Fragment> 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<Node>`].
#[inline(always)]
pub fn new(nodes: Vec<View>) -> Self {
Self::new_with_id(HydrationCtx::id(), nodes)
}
/// Creates a new [`Fragment`] from a function that returns [`Vec<Node>`].
#[inline(always)]
pub fn lazy(nodes: impl FnOnce() -> Vec<View>) -> Self {
Self::new_with_id(HydrationCtx::id(), nodes())
}
/// Creates a new [`Fragment`] with the given hydration ID from a [`Vec<Node>`].
#[inline(always)]
pub const fn new_with_id(
id: Option<HydrationKey>,
nodes: Vec<View>,
) -> 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<HydrationKey> {
&self.id
}
#[cfg(debug_assertions)]
/// Adds an optional marker indicating the view macro source.
pub fn with_view_marker(mut self, marker: impl Into<String>) -> 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()
}
}

View file

@ -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<HydrationKey>,
}
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))
}
}

View file

@ -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<AnyElement>) {
/// // do sth
/// }
///
/// // This requires an attribute value
/// fn another_directive(el: HtmlElement<AnyElement>, params: i32) {
/// // do sth
/// }
///
/// #[component]
/// pub fn MyComponent() -> impl IntoView {
/// view! {
/// // no attribute value
/// <div use:my_directive></div>
///
/// // with an attribute value
/// <div use:another_directive=8></div>
/// }
/// }
/// ```
///
/// 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<T: ?Sized, P> {
/// Calls the handler function
fn run(&self, el: HtmlElement<AnyElement>, param: P);
}
impl<F> Directive<(HtmlElement<AnyElement>,), ()> for F
where
F: Fn(HtmlElement<AnyElement>),
{
fn run(&self, el: HtmlElement<AnyElement>, _: ()) {
self(el)
}
}
impl<F, P> Directive<(HtmlElement<AnyElement>, P), P> for F
where
F: Fn(HtmlElement<AnyElement>, P),
{
fn run(&self, el: HtmlElement<AnyElement>, param: P) {
self(el, param);
}
}
impl<T: ?Sized, P> Directive<T, P> for Rc<dyn Directive<T, P>> {
fn run(&self, el: HtmlElement<AnyElement>, param: P) {
(**self).run(el, param)
}
}
impl<T: ?Sized, P> Directive<T, P> for Box<dyn Directive<T, P>> {
fn run(&self, el: HtmlElement<AnyElement>, param: P) {
(**self).run(el, param);
}
}

View file

@ -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<HashSet<Oco<'static, str>>> = RefCell::new(HashSet::new());
}
// Used in template macro
#[doc(hidden)]
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[inline(always)]
pub fn add_event_helper<E: crate::ev::EventDescriptor + 'static>(
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<E>(
target: &web_sys::Element,
key: Oco<'static, str>,
event_name: Oco<'static, str>,
#[cfg(debug_assertions)] mut cb: Box<dyn FnMut(E)>,
#[cfg(not(debug_assertions))] cb: Box<dyn FnMut(E)>,
options: &Option<web_sys::AddEventListenerOptions>,
) 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<dyn FnMut(E)>).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<E>(
target: &web_sys::Element,
event_name: &str,
#[cfg(debug_assertions)] mut cb: Box<dyn FnMut(E)>,
#[cfg(not(debug_assertions))] cb: Box<dyn FnMut(E)>,
options: &Option<web_sys::AddEventListenerOptions>,
) 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<dyn FnMut(E)>).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<web_sys::AddEventListenerOptions>,
) {
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::<js_sys::Function>();
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::<web_sys::Node>().parent_node()
{
node = parent.into()
} else if let Some(root) = node.dyn_ref::<web_sys::ShadowRoot>() {
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<dyn FnMut(web_sys::Event)>;
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);
}
})
}

View file

@ -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<web_sys::AddEventListenerOptions> {
&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<Ev: EventDescriptor>(pub Ev);
impl<Ev: EventDescriptor> EventDescriptor for undelegated<Ev> {
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<E: FromWasmAbi = web_sys::Event> {
name: Oco<'static, str>,
options: Option<web_sys::AddEventListenerOptions>,
_event_type: PhantomData<E>,
}
impl<E: FromWasmAbi> Clone for Custom<E> {
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
options: self.options.clone(),
_event_type: PhantomData,
}
}
}
impl<E: FromWasmAbi> EventDescriptor for Custom<E> {
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<web_sys::AddEventListenerOptions> {
&self.options
}
}
impl<E: FromWasmAbi> Custom<E> {
/// 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<Oco<'static, str>>) -> 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<html::Canvas> = create_node_ref();
/// # if false {
/// let mut non_passive_wheel = ev::Custom::<ev::WheelEvent>::new("wheel");
/// let options = non_passive_wheel.options_mut();
/// options.passive(false);
/// canvas_ref.on_load(move |canvas: HtmlElement<html::Canvas>| {
/// 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<E: EventDescriptor + 'static>(
self,
event: E,
handler: impl FnMut(E::EventType) + 'static,
) -> Self;
/// Same as [add](DOMEventResponder::add), but with [`EventHandler`]
#[inline]
fn add_handler(self, handler: impl EventHandler) -> Self {
handler.attach(self)
}
}
impl<T> DOMEventResponder for crate::HtmlElement<T>
where
T: crate::html::ElementDescriptor + 'static,
{
#[inline(always)]
fn add<E: EventDescriptor + 'static>(
self,
event: E,
handler: impl FnMut(E::EventType) + 'static,
) -> Self {
self.on(event, handler)
}
}
impl DOMEventResponder for crate::View {
#[inline(always)]
fn add<E: EventDescriptor + 'static>(
self,
event: E,
handler: impl FnMut(E::EventType) + 'static,
) -> Self {
self.on(event, handler)
}
}
/// A statically typed event handler.
pub enum EventHandlerFn {
/// `keydown` event handler.
Keydown(Box<dyn FnMut(KeyboardEvent)>),
/// `keyup` event handler.
Keyup(Box<dyn FnMut(KeyboardEvent)>),
/// `keypress` event handler.
Keypress(Box<dyn FnMut(KeyboardEvent)>),
/// `click` event handler.
Click(Box<dyn FnMut(MouseEvent)>),
/// `dblclick` event handler.
Dblclick(Box<dyn FnMut(MouseEvent)>),
/// `mousedown` event handler.
Mousedown(Box<dyn FnMut(MouseEvent)>),
/// `mouseup` event handler.
Mouseup(Box<dyn FnMut(MouseEvent)>),
/// `mouseenter` event handler.
Mouseenter(Box<dyn FnMut(MouseEvent)>),
/// `mouseleave` event handler.
Mouseleave(Box<dyn FnMut(MouseEvent)>),
/// `mouseout` event handler.
Mouseout(Box<dyn FnMut(MouseEvent)>),
/// `mouseover` event handler.
Mouseover(Box<dyn FnMut(MouseEvent)>),
/// `mousemove` event handler.
Mousemove(Box<dyn FnMut(MouseEvent)>),
/// `wheel` event handler.
Wheel(Box<dyn FnMut(WheelEvent)>),
/// `touchstart` event handler.
Touchstart(Box<dyn FnMut(TouchEvent)>),
/// `touchend` event handler.
Touchend(Box<dyn FnMut(TouchEvent)>),
/// `touchcancel` event handler.
Touchcancel(Box<dyn FnMut(TouchEvent)>),
/// `touchmove` event handler.
Touchmove(Box<dyn FnMut(TouchEvent)>),
/// `pointerenter` event handler.
Pointerenter(Box<dyn FnMut(PointerEvent)>),
/// `pointerleave` event handler.
Pointerleave(Box<dyn FnMut(PointerEvent)>),
/// `pointerdown` event handler.
Pointerdown(Box<dyn FnMut(PointerEvent)>),
/// `pointerup` event handler.
Pointerup(Box<dyn FnMut(PointerEvent)>),
/// `pointercancel` event handler.
Pointercancel(Box<dyn FnMut(PointerEvent)>),
/// `pointerout` event handler.
Pointerout(Box<dyn FnMut(PointerEvent)>),
/// `pointerover` event handler.
Pointerover(Box<dyn FnMut(PointerEvent)>),
/// `pointermove` event handler.
Pointermove(Box<dyn FnMut(PointerEvent)>),
/// `drag` event handler.
Drag(Box<dyn FnMut(DragEvent)>),
/// `dragend` event handler.
Dragend(Box<dyn FnMut(DragEvent)>),
/// `dragenter` event handler.
Dragenter(Box<dyn FnMut(DragEvent)>),
/// `dragleave` event handler.
Dragleave(Box<dyn FnMut(DragEvent)>),
/// `dragstart` event handler.
Dragstart(Box<dyn FnMut(DragEvent)>),
/// `drop` event handler.
Drop(Box<dyn FnMut(DragEvent)>),
/// `blur` event handler.
Blur(Box<dyn FnMut(FocusEvent)>),
/// `focusout` event handler.
Focusout(Box<dyn FnMut(FocusEvent)>),
/// `focus` event handler.
Focus(Box<dyn FnMut(FocusEvent)>),
/// `focusin` event handler.
Focusin(Box<dyn FnMut(FocusEvent)>),
}
/// Type that can be used to handle DOM events
pub trait EventHandler {
/// Attaches event listener to any target that can respond to DOM events
fn attach<T: DOMEventResponder>(self, target: T) -> T;
}
impl<T, const N: usize> EventHandler for [T; N]
where
T: EventHandler,
{
#[inline]
fn attach<R: DOMEventResponder>(self, target: R) -> R {
let mut target = target;
for item in self {
target = item.attach(target);
}
target
}
}
impl<T> EventHandler for Option<T>
where
T: EventHandler,
{
#[inline]
fn attach<R: DOMEventResponder>(self, target: R) -> R {
match self {
Some(event_handler) => event_handler.attach(target),
None => target,
}
}
}
macro_rules! tc {
($($ty:ident),*) => {
impl<$($ty),*> EventHandler for ($($ty,)*)
where
$($ty: EventHandler),*
{
#[inline]
fn attach<RES: DOMEventResponder>(self, target: RES) -> RES {
::paste::paste! {
let (
$(
[<$ty:lower>],)*
) = self;
$(
let target = [<$ty:lower>].attach(target);
)*
target
}
}
}
};
}
tc!(A);
tc!(A, B);
tc!(A, B, C);
tc!(A, B, C, D);
tc!(A, B, C, D, E);
tc!(A, B, C, D, E, F);
tc!(A, B, C, D, E, F, G);
tc!(A, B, C, D, E, F, G, H);
tc!(A, B, C, D, E, F, G, H, I);
tc!(A, B, C, D, E, F, G, H, I, J);
tc!(A, B, C, D, E, F, G, H, I, J, K);
tc!(A, B, C, D, E, F, G, H, I, J, K, L);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y);
#[rustfmt::skip]
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z);
macro_rules! collection_callback {
{$(
$collection:ident
),* $(,)?} => {
$(
impl<T> EventHandler for $collection<T>
where
T: EventHandler
{
#[inline]
fn attach<R: DOMEventResponder>(self, target: R) -> R {
let mut target = target;
for item in self {
target = item.attach(target);
}
target
}
}
)*
};
}
use std::collections::{BTreeSet, BinaryHeap, HashSet, LinkedList, VecDeque};
collection_callback! {
Vec,
BTreeSet,
BinaryHeap,
HashSet,
LinkedList,
VecDeque,
}
macro_rules! generate_event_types {
{$(
$( #[$does_not_bubble:ident] )?
$( $event:ident )+ : $web_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<dyn FnMut($web_event) + 'static>),
)*
}
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::<Box<dyn FnMut($web_event) + 'static>>())
.finish(),
)*
}
}
}
impl EventHandler for GenericEventHandler {
fn attach<T: DOMEventResponder>(self, target: T) -> T {
match self {
$(
Self::[< $($event:camel)+ >](event, handler) => target.add(event, handler),
)*
}
}
}
$(
impl<F> From<([< $($event)+ >], F)> for GenericEventHandler
where
F: FnMut($web_event) + 'static
{
fn from(value: ([< $($event)+ >], F)) -> Self {
Self::[< $($event:camel)+ >](value.0, Box::new(value.1))
}
}
// NOTE: this could become legal in future and would save us from useless allocations
//impl<F> From<([< $($event)+ >], Box<F>)> for GenericEventHandler
//where
// F: FnMut($web_event) + 'static
//{
// fn from(value: ([< $($event)+ >], Box<F>)) -> Self {
// Self::[< $($event:camel)+ >](value.0, value.1)
// }
//}
impl<F> EventHandler for ([< $($event)+ >], F)
where
F: FnMut($web_event) + 'static
{
fn attach<L: DOMEventResponder>(self, target: L) -> L {
target.add(self.0, self.1)
}
}
)*
}
};
(does_not_bubble) => { false }
}
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,
};

File diff suppressed because it is too large Load diff

View file

@ -1,312 +0,0 @@
#[cfg(all(
feature = "experimental-islands",
any(feature = "hydrate", feature = "ssr")
))]
use leptos_reactive::SharedContext;
use std::{cell::RefCell, fmt::Display};
#[cfg(feature = "hydrate")]
mod hydrate_only {
use once_cell::unsync::Lazy as LazyCell;
use std::{cell::Cell, collections::HashMap};
use wasm_bindgen::JsCast;
/// See ["createTreeWalker"](https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker)
#[allow(unused)]
const FILTER_SHOW_COMMENT: u32 = 0b10000000;
thread_local! {
pub static HYDRATION_COMMENTS: LazyCell<HashMap<String, web_sys::Comment>> = LazyCell::new(|| {
let document = crate::document();
let body = document.body().unwrap();
let walker = document
.create_tree_walker_with_what_to_show(&body, FILTER_SHOW_COMMENT)
.unwrap();
let mut map = HashMap::new();
while let Ok(Some(node)) = walker.next_node() {
if let Some(content) = node.text_content() {
if let Some(hk) = content.strip_prefix("hk=") {
if let Some(hk) = hk.split('|').next() {
map.insert(hk.into(), node.unchecked_into());
}
}
}
}
map
});
pub static HYDRATION_ELEMENTS: LazyCell<HashMap<String, web_sys::HtmlElement>> = LazyCell::new(|| {
let document = crate::document();
let els = document.query_selector_all("[data-hk]");
if let Ok(list) = els {
let len = list.length();
let mut map = HashMap::with_capacity(len as usize);
for idx in 0..len {
let el = list.item(idx).unwrap().unchecked_into::<web_sys::HtmlElement>();
let dataset = el.dataset();
let hk = dataset.get(wasm_bindgen::intern("hk")).unwrap();
map.insert(hk, el);
}
map
} else {
Default::default()
}
});
pub static IS_HYDRATING: Cell<bool> = const { Cell::new(true) };
}
#[allow(unused)]
pub fn get_marker(id: &str) -> Option<web_sys::Comment> {
HYDRATION_COMMENTS.with(|comments| comments.get(id).cloned())
}
#[allow(unused)]
pub fn get_element(hk: &str) -> Option<web_sys::HtmlElement> {
HYDRATION_ELEMENTS.with(|els| els.get(hk).cloned())
}
}
#[cfg(feature = "hydrate")]
pub(crate) use hydrate_only::*;
/// A stable identifier within the server-rendering or hydration process.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct HydrationKey {
/// ID of the current outlet
pub outlet: usize,
/// ID of the current fragment.
pub fragment: usize,
/// ID of the current error boundary.
pub error: usize,
/// ID of the current key.
pub id: usize,
}
impl Display for HydrationKey {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{}-{}-{}-{}",
self.outlet, self.fragment, self.error, self.id
)
}
}
impl std::str::FromStr for HydrationKey {
type Err = (); // TODO better error
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut pieces = s.splitn(4, '-');
let first = pieces.next().ok_or(())?;
let second = pieces.next().ok_or(())?;
let third = pieces.next().ok_or(())?;
let fourth = pieces.next().ok_or(())?;
let outlet = usize::from_str(first).map_err(|_| ())?;
let fragment = usize::from_str(second).map_err(|_| ())?;
let error = usize::from_str(third).map_err(|_| ())?;
let id = usize::from_str(fourth).map_err(|_| ())?;
Ok(HydrationKey {
outlet,
fragment,
error,
id,
})
}
}
#[cfg(test)]
mod tests {
#[test]
fn parse_hydration_key() {
use crate::HydrationKey;
use std::str::FromStr;
assert_eq!(
HydrationKey::from_str("0-1-2-3"),
Ok(HydrationKey {
outlet: 0,
fragment: 1,
error: 2,
id: 3
})
)
}
}
thread_local!(static ID: RefCell<HydrationKey> = const {RefCell::new(HydrationKey { outlet: 0, fragment: 0, error: 0, id: 0 })});
/// Control and utility methods for hydration.
pub struct HydrationCtx;
impl HydrationCtx {
/// If you're in an hydration context, get the next `id` without incrementing it.
pub fn peek() -> Option<HydrationKey> {
#[cfg(all(
feature = "experimental-islands",
any(feature = "hydrate", feature = "ssr")
))]
let no_hydrate = SharedContext::no_hydrate();
#[cfg(not(all(
feature = "experimental-islands",
any(feature = "hydrate", feature = "ssr")
)))]
let no_hydrate = false;
if no_hydrate {
None
} else {
Some(ID.with(|id| *id.borrow()))
}
}
/// Get the next `id` without incrementing it.
pub fn peek_always() -> HydrationKey {
ID.with(|id| *id.borrow())
}
/// Increments the current hydration `id` and returns it
pub fn id() -> Option<HydrationKey> {
#[cfg(all(
feature = "experimental-islands",
any(feature = "hydrate", feature = "ssr")
))]
let no_hydrate = SharedContext::no_hydrate();
#[cfg(not(all(
feature = "experimental-islands",
any(feature = "hydrate", feature = "ssr")
)))]
let no_hydrate = false;
if no_hydrate {
None
} else {
Some(ID.with(|id| {
let mut id = id.borrow_mut();
id.id = id.id.wrapping_add(1);
*id
}))
}
}
/// Resets the hydration `id` for the next component, and returns it
pub fn next_component() -> HydrationKey {
ID.with(|id| {
let mut id = id.borrow_mut();
id.fragment = id.fragment.wrapping_add(1);
id.id = 0;
*id
})
}
/// Resets the hydration `id` for the next outlet, and returns it
pub fn next_outlet() -> HydrationKey {
ID.with(|id| {
let mut id = id.borrow_mut();
id.outlet = id.outlet.wrapping_add(1);
id.id = 0;
*id
})
}
/// Resets the hydration `id` for the next component, and returns it
pub fn next_error() -> HydrationKey {
ID.with(|id| {
let mut id = id.borrow_mut();
id.error = id.error.wrapping_add(1);
id.id = 0;
*id
})
}
#[doc(hidden)]
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub fn reset_id() {
ID.with(|id| {
*id.borrow_mut() = HydrationKey {
outlet: 0,
fragment: 0,
error: 0,
id: 0,
}
});
}
/// Resumes hydration from the provided `id`. Useful for
/// `Suspense` and other fancy things.
pub fn continue_from(id: HydrationKey) {
ID.with(|i| *i.borrow_mut() = id);
}
/// Resumes hydration after the provided `id`. Useful for
/// islands and other fancy things.
pub fn continue_after(id: HydrationKey) {
ID.with(|i| {
*i.borrow_mut() = HydrationKey {
outlet: id.outlet,
fragment: id.fragment,
error: id.error,
id: id.id + 1,
}
});
}
#[doc(hidden)]
pub fn stop_hydrating() {
#[cfg(feature = "hydrate")]
{
IS_HYDRATING.with(|is_hydrating| {
is_hydrating.set(false);
})
}
}
#[doc(hidden)]
#[cfg(feature = "hydrate")]
pub fn with_hydration_on<T>(f: impl FnOnce() -> T) -> T {
let prev = IS_HYDRATING.with(|is_hydrating| {
let prev = is_hydrating.get();
is_hydrating.set(true);
prev
});
let value = f();
IS_HYDRATING.with(|is_hydrating| is_hydrating.set(prev));
value
}
#[doc(hidden)]
#[cfg(feature = "hydrate")]
pub fn with_hydration_off<T>(f: impl FnOnce() -> T) -> T {
let prev = IS_HYDRATING.with(|is_hydrating| {
let prev = is_hydrating.get();
is_hydrating.set(false);
prev
});
let value = f();
IS_HYDRATING.with(|is_hydrating| is_hydrating.set(prev));
value
}
/// Whether the UI is currently in the process of hydrating from the server-sent HTML.
#[inline(always)]
pub fn is_hydrating() -> bool {
#[cfg(feature = "hydrate")]
{
IS_HYDRATING.with(|is_hydrating| is_hydrating.get())
}
#[cfg(not(feature = "hydrate"))]
{
false
}
}
#[cfg(feature = "hydrate")]
#[allow(unused)]
pub(crate) fn to_string(id: &HydrationKey, closing: bool) -> String {
#[cfg(debug_assertions)]
return format!("{id}{}", if closing { 'c' } else { 'o' });
#[cfg(not(debug_assertions))]
{
id.to_string()
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,421 +0,0 @@
#[cfg(not(feature = "nightly"))]
use leptos_reactive::{
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
use leptos_reactive::{Oco, TextProp};
use std::{borrow::Cow, rc::Rc};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use wasm_bindgen::UnwrapThrowExt;
/// Represents the different possible values an attribute node could have.
///
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
/// macros use. You usually won't need to interact with it directly, but it can be useful for defining
/// permissive APIs for certain components.
#[derive(Clone)]
pub enum Attribute {
/// A plain string value.
String(Oco<'static, str>),
/// A (presumably reactive) function, which will be run inside an effect to do targeted updates to the attribute.
Fn(Rc<dyn Fn() -> Attribute>),
/// An optional string value, which sets the attribute to the value if `Some` and removes the attribute if `None`.
Option(Option<Oco<'static, str>>),
/// A boolean attribute, which sets the attribute if `true` and removes the attribute if `false`.
Bool(bool),
}
impl Attribute {
/// Converts the attribute to its HTML value at that moment, including the attribute name,
/// so it can be rendered on the server.
pub fn as_value_string(
&self,
attr_name: &'static str,
) -> Oco<'static, str> {
match self {
Attribute::String(value) => {
format!("{attr_name}=\"{value}\"").into()
}
Attribute::Fn(f) => {
let mut value = f();
while let Attribute::Fn(f) = value {
value = f();
}
value.as_value_string(attr_name)
}
Attribute::Option(value) => value
.as_ref()
.map(|value| format!("{attr_name}=\"{value}\"").into())
.unwrap_or_default(),
Attribute::Bool(include) => {
Oco::Borrowed(if *include { attr_name } else { "" })
}
}
}
/// Converts the attribute to its HTML value at that moment, not including
/// the attribute name, so it can be rendered on the server.
pub fn as_nameless_value_string(&self) -> Option<Oco<'static, str>> {
match self {
Attribute::String(value) => Some(value.clone()),
Attribute::Fn(f) => {
let mut value = f();
while let Attribute::Fn(f) = value {
value = f();
}
value.as_nameless_value_string()
}
Attribute::Option(value) => value.as_ref().cloned(),
Attribute::Bool(include) => {
if *include {
Some("".into())
} else {
None
}
}
}
}
}
impl PartialEq for Attribute {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::String(l0), Self::String(r0)) => l0 == r0,
(Self::Fn(_), Self::Fn(_)) => false,
(Self::Option(l0), Self::Option(r0)) => l0 == r0,
(Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
_ => false,
}
}
}
impl core::fmt::Debug for Attribute {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::String(arg0) => f.debug_tuple("String").field(arg0).finish(),
Self::Fn(_) => f.debug_tuple("Fn").finish(),
Self::Option(arg0) => f.debug_tuple("Option").field(arg0).finish(),
Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(),
}
}
}
/// Converts some type into an [`Attribute`].
///
/// This is implemented by default for Rust primitive and string types.
pub trait IntoAttribute {
/// Converts the object into an [`Attribute`].
fn into_attribute(self) -> Attribute;
/// Helper function for dealing with `Box<dyn IntoAttribute>`.
fn into_attribute_boxed(self: Box<Self>) -> Attribute;
}
impl<T: IntoAttribute + 'static> From<T> for Box<dyn IntoAttribute> {
#[inline(always)]
fn from(value: T) -> Self {
Box::new(value)
}
}
impl IntoAttribute for Attribute {
#[inline(always)]
fn into_attribute(self) -> Attribute {
self
}
#[inline(always)]
fn into_attribute_boxed(self: Box<Self>) -> Attribute {
*self
}
}
macro_rules! impl_into_attr_boxed {
() => {
#[inline(always)]
fn into_attribute_boxed(self: Box<Self>) -> Attribute {
self.into_attribute()
}
};
}
impl IntoAttribute for String {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::String(Oco::Owned(self))
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for &'static str {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::String(Oco::Borrowed(self))
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for Cow<'static, str> {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::String(self.into())
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for Oco<'static, str> {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::String(self)
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for Rc<str> {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::String(Oco::Counted(self))
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for bool {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::Bool(self)
}
impl_into_attr_boxed! {}
}
impl<T: IntoAttribute> IntoAttribute for Option<T> {
#[inline(always)]
fn into_attribute(self) -> Attribute {
self.map_or(Attribute::Option(None), IntoAttribute::into_attribute)
}
impl_into_attr_boxed! {}
}
impl<T, U> IntoAttribute for T
where
T: Fn() -> U + 'static,
U: IntoAttribute,
{
fn into_attribute(self) -> Attribute {
let modified_fn = Rc::new(move || (self)().into_attribute());
Attribute::Fn(modified_fn)
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for Option<Box<dyn IntoAttribute>> {
fn into_attribute(self) -> Attribute {
match self {
Some(bx) => bx.into_attribute_boxed(),
None => Attribute::Option(None),
}
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for TextProp {
fn into_attribute(self) -> Attribute {
(move || self.get()).into_attribute()
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for core::fmt::Arguments<'_> {
fn into_attribute(self) -> Attribute {
match self.as_str() {
Some(s) => s.into_attribute(),
None => self.to_string().into_attribute(),
}
}
impl_into_attr_boxed! {}
}
/* impl IntoAttribute for Box<dyn IntoAttribute> {
#[inline(always)]
fn into_attribute(self) -> Attribute {
self.into_attribute_boxed()
}
impl_into_attr_boxed! {}
} */
macro_rules! attr_type {
($attr_type:ty) => {
impl IntoAttribute for $attr_type {
fn into_attribute(self) -> Attribute {
Attribute::String(self.to_string().into())
}
#[inline]
fn into_attribute_boxed(self: Box<Self>) -> Attribute {
self.into_attribute()
}
}
};
}
macro_rules! attr_signal_type {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl<T> IntoAttribute for $signal_type
where
T: IntoAttribute + Clone,
{
fn into_attribute(self) -> Attribute {
let modified_fn = Rc::new(move || self.get().into_attribute());
Attribute::Fn(modified_fn)
}
impl_into_attr_boxed! {}
}
};
}
attr_type!(&String);
attr_type!(usize);
attr_type!(u8);
attr_type!(u16);
attr_type!(u32);
attr_type!(u64);
attr_type!(u128);
attr_type!(isize);
attr_type!(i8);
attr_type!(i16);
attr_type!(i32);
attr_type!(i64);
attr_type!(i128);
attr_type!(f32);
attr_type!(f64);
attr_type!(char);
attr_signal_type!(ReadSignal<T>);
attr_signal_type!(RwSignal<T>);
attr_signal_type!(Memo<T>);
attr_signal_type!(Signal<T>);
attr_signal_type!(MaybeSignal<T>);
attr_signal_type!(MaybeProp<T>);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[doc(hidden)]
#[inline(never)]
#[track_caller]
pub fn attribute_helper(
el: &web_sys::Element,
name: Oco<'static, str>,
value: Attribute,
) {
#[cfg(debug_assertions)]
let called_at = std::panic::Location::caller();
use leptos_reactive::create_render_effect;
match value {
Attribute::Fn(f) => {
let el = el.clone();
create_render_effect(move |old| {
let new = f();
if old.as_ref() != Some(&new) {
attribute_expression(
&el,
&name,
new.clone(),
true,
#[cfg(debug_assertions)]
called_at,
);
}
new
});
}
_ => attribute_expression(
el,
&name,
value,
false,
#[cfg(debug_assertions)]
called_at,
),
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[inline(never)]
pub(crate) fn attribute_expression(
el: &web_sys::Element,
attr_name: &str,
value: Attribute,
force: bool,
#[cfg(debug_assertions)] called_at: &'static std::panic::Location<'static>,
) {
use crate::HydrationCtx;
if force || !HydrationCtx::is_hydrating() {
match value {
Attribute::String(value) => {
let value = wasm_bindgen::intern(&value);
if attr_name == "inner_html" {
el.set_inner_html(value);
} else {
let attr_name = wasm_bindgen::intern(attr_name);
el.set_attribute(attr_name, value).unwrap_throw();
}
}
Attribute::Option(value) => {
if attr_name == "inner_html" {
el.set_inner_html(&value.unwrap_or_default());
} else {
let attr_name = wasm_bindgen::intern(attr_name);
match value {
Some(value) => {
let value = wasm_bindgen::intern(&value);
el.set_attribute(attr_name, value).unwrap_throw();
}
None => el.remove_attribute(attr_name).unwrap_throw(),
}
}
}
Attribute::Bool(value) => {
let attr_name = wasm_bindgen::intern(attr_name);
if value {
el.set_attribute(attr_name, attr_name).unwrap_throw();
} else {
el.remove_attribute(attr_name).unwrap_throw();
}
}
Attribute::Fn(f) => {
let mut v = f();
crate::debug_warn!(
"At {called_at}, you are providing a dynamic attribute \
with a nested function. For example, you might have a \
closure that returns another function instead of a \
value. This creates some added overhead. If possible, \
you should instead provide a function that returns a \
value instead.",
);
while let Attribute::Fn(f) = v {
v = f();
}
attribute_expression(
el,
attr_name,
v,
force,
#[cfg(debug_assertions)]
called_at,
);
}
}
}
}

View file

@ -1,172 +0,0 @@
#[cfg(not(feature = "nightly"))]
use leptos_reactive::{
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
/// Represents the different possible values a single class on an element could have,
/// allowing you to do fine-grained updates to single items
/// in [`Element.classList`](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList).
///
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
/// macros use. You usually won't need to interact with it directly, but it can be useful for defining
/// permissive APIs for certain components.
pub enum Class {
/// Whether the class is present.
Value(bool),
/// A (presumably reactive) function, which will be run inside an effect to toggle the class.
Fn(Box<dyn Fn() -> bool>),
}
/// Converts some type into a [`Class`].
pub trait IntoClass {
/// Converts the object into a [`Class`].
fn into_class(self) -> Class;
/// Helper function for dealing with `Box<dyn IntoClass>`.
fn into_class_boxed(self: Box<Self>) -> Class;
}
impl IntoClass for bool {
#[inline(always)]
fn into_class(self) -> Class {
Class::Value(self)
}
fn into_class_boxed(self: Box<Self>) -> Class {
(*self).into_class()
}
}
impl<T> IntoClass for T
where
T: Fn() -> bool + 'static,
{
#[inline(always)]
fn into_class(self) -> Class {
let modified_fn = Box::new(self);
Class::Fn(modified_fn)
}
fn into_class_boxed(self: Box<Self>) -> Class {
(*self).into_class()
}
}
impl Class {
/// Converts the class to its HTML value at that moment so it can be rendered on the server.
pub fn as_value_string(&self, class_name: &'static str) -> &'static str {
match self {
Class::Value(value) => {
if *value {
class_name
} else {
""
}
}
Class::Fn(f) => {
let value = f();
if value {
class_name
} else {
""
}
}
}
}
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use leptos_reactive::Oco;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[doc(hidden)]
#[inline(never)]
pub fn class_helper(
el: &web_sys::Element,
name: Oco<'static, str>,
value: Class,
) {
use leptos_reactive::create_render_effect;
let class_list = el.class_list();
match value {
Class::Fn(f) => {
create_render_effect(move |old| {
let new = f();
if old.as_ref() != Some(&new) && (old.is_some() || new) {
class_expression(&class_list, &name, new, true)
}
new
});
}
Class::Value(value) => {
class_expression(&class_list, &name, value, false)
}
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[inline(never)]
pub(crate) fn class_expression(
class_list: &web_sys::DomTokenList,
class_name: &str,
value: bool,
force: bool,
) {
use crate::HydrationCtx;
if force || !HydrationCtx::is_hydrating() {
let class_name = wasm_bindgen::intern(class_name);
if value {
if let Err(e) = class_list.add_1(class_name) {
crate::error!("[HtmlElement::class()] {e:?}");
}
} else {
if let Err(e) = class_list.remove_1(class_name) {
crate::error!("[HtmlElement::class()] {e:?}");
}
}
}
}
macro_rules! class_signal_type {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl IntoClass for $signal_type {
#[inline(always)]
fn into_class(self) -> Class {
let modified_fn = Box::new(move || self.get());
Class::Fn(modified_fn)
}
fn into_class_boxed(self: Box<Self>) -> Class {
(*self).into_class()
}
}
};
}
macro_rules! class_signal_type_optional {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl IntoClass for $signal_type {
#[inline(always)]
fn into_class(self) -> Class {
let modified_fn = Box::new(move || self.get().unwrap_or(false));
Class::Fn(modified_fn)
}
fn into_class_boxed(self: Box<Self>) -> Class {
(*self).into_class()
}
}
};
}
class_signal_type!(ReadSignal<bool>);
class_signal_type!(RwSignal<bool>);
class_signal_type!(Memo<bool>);
class_signal_type!(Signal<bool>);
class_signal_type!(MaybeSignal<bool>);
class_signal_type_optional!(MaybeProp<bool>);

View file

@ -1,178 +0,0 @@
#[cfg(not(feature = "nightly"))]
use leptos_reactive::{
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
use wasm_bindgen::JsValue;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use wasm_bindgen::UnwrapThrowExt;
/// Represents the different possible values an element property could have,
/// allowing you to do fine-grained updates to single fields.
///
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
/// macros use. You usually won't need to interact with it directly, but it can be useful for defining
/// permissive APIs for certain components.
pub enum Property {
/// A static JavaScript value.
Value(JsValue),
/// A (presumably reactive) function, which will be run inside an effect to update the property.
Fn(Box<dyn Fn() -> JsValue>),
}
/// Converts some type into a [`Property`].
///
/// This is implemented by default for Rust primitive types, [`String`] and friends, and [`JsValue`].
pub trait IntoProperty {
/// Converts the object into a [`Property`].
fn into_property(self) -> Property;
/// Helper function for dealing with `Box<dyn IntoProperty>`.
fn into_property_boxed(self: Box<Self>) -> Property;
}
impl<T, U> IntoProperty for T
where
T: Fn() -> U + 'static,
U: Into<JsValue>,
{
fn into_property(self) -> Property {
let modified_fn = Box::new(move || self().into());
Property::Fn(modified_fn)
}
fn into_property_boxed(self: Box<Self>) -> Property {
(*self).into_property()
}
}
macro_rules! prop_type {
($prop_type:ty) => {
impl IntoProperty for $prop_type {
#[inline(always)]
fn into_property(self) -> Property {
Property::Value(self.into())
}
fn into_property_boxed(self: Box<Self>) -> Property {
(*self).into_property()
}
}
impl IntoProperty for Option<$prop_type> {
#[inline(always)]
fn into_property(self) -> Property {
Property::Value(self.into())
}
fn into_property_boxed(self: Box<Self>) -> Property {
(*self).into_property()
}
}
};
}
macro_rules! prop_signal_type {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl<T> IntoProperty for $signal_type
where
T: Into<JsValue> + Clone,
{
fn into_property(self) -> Property {
let modified_fn = Box::new(move || self.get().into());
Property::Fn(modified_fn)
}
fn into_property_boxed(self: Box<Self>) -> Property {
(*self).into_property()
}
}
};
}
macro_rules! prop_signal_type_optional {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl<T> IntoProperty for $signal_type
where
T: Clone,
Option<T>: Into<JsValue>,
{
fn into_property(self) -> Property {
let modified_fn = Box::new(move || self.get().into());
Property::Fn(modified_fn)
}
fn into_property_boxed(self: Box<Self>) -> Property {
(*self).into_property()
}
}
};
}
prop_type!(JsValue);
prop_type!(String);
prop_type!(&String);
prop_type!(&str);
prop_type!(usize);
prop_type!(u8);
prop_type!(u16);
prop_type!(u32);
prop_type!(u64);
prop_type!(u128);
prop_type!(isize);
prop_type!(i8);
prop_type!(i16);
prop_type!(i32);
prop_type!(i64);
prop_type!(i128);
prop_type!(f32);
prop_type!(f64);
prop_type!(bool);
prop_signal_type!(ReadSignal<T>);
prop_signal_type!(RwSignal<T>);
prop_signal_type!(Memo<T>);
prop_signal_type!(Signal<T>);
prop_signal_type!(MaybeSignal<T>);
prop_signal_type_optional!(MaybeProp<T>);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use leptos_reactive::Oco;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[inline(never)]
pub(crate) fn property_helper(
el: &web_sys::Element,
name: Oco<'static, str>,
value: Property,
) {
use leptos_reactive::create_render_effect;
match value {
Property::Fn(f) => {
let el = el.clone();
create_render_effect(move |_| {
let new = f();
let prop_name = wasm_bindgen::intern(&name);
property_expression(&el, prop_name, new.clone());
new
});
}
Property::Value(value) => {
let prop_name = wasm_bindgen::intern(&name);
property_expression(el, prop_name, value)
}
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[inline(never)]
pub(crate) fn property_expression(
el: &web_sys::Element,
prop_name: &str,
value: JsValue,
) {
js_sys::Reflect::set(el, &JsValue::from_str(prop_name), &value)
.unwrap_throw();
}

View file

@ -1,360 +0,0 @@
use leptos_reactive::Oco;
#[cfg(not(feature = "nightly"))]
use leptos_reactive::{
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
use std::{borrow::Cow, rc::Rc};
/// todo docs
#[derive(Clone)]
pub enum Style {
/// A plain string value.
Value(Oco<'static, str>),
/// An optional string value, which sets the property to the value if `Some` and removes the property if `None`.
Option(Option<Oco<'static, str>>),
/// A (presumably reactive) function, which will be run inside an effect to update the style.
Fn(Rc<dyn Fn() -> Style>),
}
impl PartialEq for Style {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Value(l0), Self::Value(r0)) => l0 == r0,
(Self::Fn(_), Self::Fn(_)) => false,
(Self::Option(l0), Self::Option(r0)) => l0 == r0,
_ => false,
}
}
}
impl core::fmt::Debug for Style {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Value(arg0) => f.debug_tuple("Value").field(arg0).finish(),
Self::Fn(_) => f.debug_tuple("Fn").finish(),
Self::Option(arg0) => f.debug_tuple("Option").field(arg0).finish(),
}
}
}
/// Converts some type into a [`Style`].
pub trait IntoStyle {
/// Converts the object into a [`Style`].
fn into_style(self) -> Style;
/// Helper function for dealing with `Box<dyn IntoStyle>`.
fn into_style_boxed(self: Box<Self>) -> Style;
}
impl IntoStyle for Style {
fn into_style(self) -> Style {
self
}
fn into_style_boxed(self: Box<Self>) -> Style {
*self
}
}
impl IntoStyle for &'static str {
#[inline(always)]
fn into_style(self) -> Style {
Style::Value(Oco::Borrowed(self))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for String {
#[inline(always)]
fn into_style(self) -> Style {
Style::Value(Oco::Owned(self))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Rc<str> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Value(Oco::Counted(self))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(self).into_style()
}
}
impl IntoStyle for Cow<'static, str> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Value(self.into())
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Oco<'static, str> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Value(self)
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Option<&'static str> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Option(self.map(Oco::Borrowed))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Option<String> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Option(self.map(Oco::Owned))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(self).into_style()
}
}
impl IntoStyle for Option<Rc<str>> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Option(self.map(Oco::Counted))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Option<Cow<'static, str>> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Option(self.map(Oco::from))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Option<Oco<'static, str>> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Option(self)
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl<T, U> IntoStyle for T
where
T: Fn() -> U + 'static,
U: IntoStyle,
{
#[inline(always)]
fn into_style(self) -> Style {
let modified_fn = Rc::new(move || (self)().into_style());
Style::Fn(modified_fn)
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl Style {
/// Converts the style to its HTML value at that moment so it can be rendered on the server.
pub fn as_value_string(
&self,
style_name: &str,
) -> Option<Oco<'static, str>> {
match self {
Style::Value(value) => {
Some(format!("{style_name}: {value};").into())
}
Style::Option(value) => value
.as_ref()
.map(|value| format!("{style_name}: {value};").into()),
Style::Fn(f) => {
let mut value = f();
while let Style::Fn(f) = value {
value = f();
}
value.as_value_string(style_name)
}
}
}
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[doc(hidden)]
#[inline(never)]
pub fn style_helper(
el: &web_sys::Element,
name: Oco<'static, str>,
value: Style,
) {
use leptos_reactive::create_render_effect;
use std::ops::Deref;
use wasm_bindgen::JsCast;
let el = el.unchecked_ref::<web_sys::HtmlElement>();
let style_list = el.style();
match value {
Style::Fn(f) => {
create_render_effect(move |old| {
let mut new = f();
while let Style::Fn(f) = new {
new = f();
}
let new = match new {
Style::Value(value) => Some(value),
Style::Option(value) => value,
_ => unreachable!(),
};
if old.as_ref() != Some(&new) {
style_expression(&style_list, &name, new.as_deref(), true)
}
new
});
}
Style::Value(value) => {
style_expression(&style_list, &name, Some(value.deref()), false)
}
Style::Option(value) => {
style_expression(&style_list, &name, value.as_deref(), false)
}
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[inline(never)]
pub(crate) fn style_expression(
style_list: &web_sys::CssStyleDeclaration,
style_name: &str,
value: Option<&str>,
force: bool,
) {
use crate::HydrationCtx;
if force || !HydrationCtx::is_hydrating() {
let style_name = wasm_bindgen::intern(style_name);
if let Some(value) = value {
if let Err(e) = style_list.set_property(style_name, value) {
crate::error!("[HtmlElement::style()] {e:?}");
}
} else {
if let Err(e) = style_list.remove_property(style_name) {
crate::error!("[HtmlElement::style()] {e:?}");
}
}
}
}
macro_rules! style_type {
($style_type:ty) => {
impl IntoStyle for $style_type {
fn into_style(self) -> Style {
Style::Value(self.to_string().into())
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Option<$style_type> {
fn into_style(self) -> Style {
Style::Option(self.map(|n| n.to_string().into()))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
};
}
macro_rules! style_signal_type {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl<T> IntoStyle for $signal_type
where
T: IntoStyle + Clone,
{
fn into_style(self) -> Style {
let modified_fn = Rc::new(move || self.get().into_style());
Style::Fn(modified_fn)
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
};
}
macro_rules! style_signal_type_optional {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl<T> IntoStyle for $signal_type
where
T: Clone,
Option<T>: IntoStyle,
{
fn into_style(self) -> Style {
let modified_fn = Rc::new(move || self.get().into_style());
Style::Fn(modified_fn)
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
};
}
style_type!(&String);
style_type!(usize);
style_type!(u8);
style_type!(u16);
style_type!(u32);
style_type!(u64);
style_type!(u128);
style_type!(isize);
style_type!(i8);
style_type!(i16);
style_type!(i32);
style_type!(i64);
style_type!(i128);
style_type!(f32);
style_type!(f64);
style_type!(char);
style_signal_type!(ReadSignal<T>);
style_signal_type!(RwSignal<T>);
style_signal_type!(Memo<T>);
style_signal_type!(Signal<T>);
style_signal_type!(MaybeSignal<T>);
style_signal_type_optional!(MaybeProp<T>);

View file

@ -1,11 +0,0 @@
mod into_attribute;
mod into_class;
mod into_property;
mod into_style;
#[cfg(feature = "trace-component-props")]
#[doc(hidden)]
pub mod tracing_property;
pub use into_attribute::*;
pub use into_class::*;
pub use into_property::*;
pub use into_style::*;

View file

@ -1,176 +0,0 @@
use wasm_bindgen::UnwrapThrowExt;
#[macro_export]
/// Use for tracing property
macro_rules! tracing_props {
() => {
::leptos::leptos_dom::tracing::span!(
::leptos::leptos_dom::tracing::Level::TRACE,
"leptos_dom::tracing_props",
props = String::from("[]")
);
};
($($prop:tt),+ $(,)?) => {
{
use ::leptos::leptos_dom::tracing_property::{Match, SerializeMatch, DefaultMatch};
let mut props = String::from('[');
$(
let prop = (&&Match {
name: stringify!{$prop},
value: std::cell::Cell::new(Some(&$prop))
}).spez();
props.push_str(&format!("{prop},"));
)*
props.pop();
props.push(']');
::leptos::leptos_dom::tracing::span!(
::leptos::leptos_dom::tracing::Level::TRACE,
"leptos_dom::tracing_props",
props
);
}
};
}
// Implementation based on spez
// see https://github.com/m-ou-se/spez
pub struct Match<T> {
pub name: &'static str,
pub value: std::cell::Cell<Option<T>>,
}
pub trait SerializeMatch {
type Return;
fn spez(&self) -> Self::Return;
}
impl<T: serde::Serialize> SerializeMatch for &Match<&T> {
type Return = String;
fn spez(&self) -> Self::Return {
let name = self.name;
// suppresses warnings when serializing signals into props
#[cfg(debug_assertions)]
let prev = leptos_reactive::SpecialNonReactiveZone::enter();
let value = serde_json::to_string(self.value.get().unwrap_throw())
.map_or_else(
|err| format!(r#"{{"name": "{name}", "error": "{err}"}}"#),
|value| format!(r#"{{"name": "{name}", "value": {value}}}"#),
);
#[cfg(debug_assertions)]
leptos_reactive::SpecialNonReactiveZone::exit(prev);
value
}
}
pub trait DefaultMatch {
type Return;
fn spez(&self) -> Self::Return;
}
impl<T> DefaultMatch for Match<&T> {
type Return = String;
fn spez(&self) -> Self::Return {
let name = self.name;
format!(r#"{{"name": "{name}", "value": "[unserializable value]"}}"#)
}
}
#[test]
fn match_primitive() {
// String
let test = String::from("string");
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": "string"}"#);
// &str
let test = "string";
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": "string"}"#);
// u128
let test: u128 = 1;
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": 1}"#);
// i128
let test: i128 = -1;
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": -1}"#);
// f64
let test = 3.25;
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": 3.25}"#);
// bool
let test = true;
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": true}"#);
}
#[test]
fn match_serialize() {
use serde::Serialize;
#[derive(Serialize)]
struct CustomStruct {
field: &'static str,
}
let test = CustomStruct { field: "field" };
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": {"field":"field"}}"#);
// Verification of ownership
assert_eq!(test.field, "field");
}
#[test]
#[allow(clippy::needless_borrow)]
fn match_no_serialize() {
struct CustomStruct {
field: &'static str,
}
let test = CustomStruct { field: "field" };
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(
prop,
r#"{"name": "test", "value": "[unserializable value]"}"#
);
// Verification of ownership
assert_eq!(test.field, "field");
}

View file

@ -1,238 +0,0 @@
//! Exports types for working with MathML elements.
use super::{AnyElement, ElementDescriptor, HtmlElement};
use crate::HydrationCtx;
use cfg_if::cfg_if;
use leptos_reactive::Oco;
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use once_cell::unsync::Lazy as LazyCell;
use wasm_bindgen::JsCast;
} else {
use super::{HydrationKey, html::HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG};
}
}
macro_rules! generate_math_tags {
(
$(
#[$meta:meta]
$(#[$void:ident])?
$tag:ident $(- $second:ident $(- $third:ident)?)? $(@ $trailing_:pat)?
),* $(,)?
) => {
paste::paste! {
$(
#[cfg(all(target_arch = "wasm32", feature = "web"))]
thread_local! {
static [<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]: LazyCell<web_sys::HtmlElement> = LazyCell::new(|| {
crate::document()
.create_element_ns(
Some(wasm_bindgen::intern("http://www.w3.org/1998/Math/MathML")),
concat![
stringify!($tag),
$(
"-", stringify!($second),
$(
"-", stringify!($third)
)?
)?
],
)
.unwrap()
.unchecked_into()
});
}
#[derive(Clone, Debug)]
#[$meta]
pub struct [<$tag:camel $($second:camel $($third:camel)?)?>] {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
element: web_sys::HtmlElement,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id: Option<HydrationKey>,
}
impl Default for [<$tag:camel $($second:camel $($third:camel)?)?>] {
fn default() -> Self {
#[allow(unused)]
let id = HydrationCtx::id();
#[cfg(all(target_arch = "wasm32", feature = "hydrate"))]
let element = 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(),
stringify!([<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]),
"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 {
crate::warn!(
"element with id {id} not found, ignoring it for hydration"
);
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]
.with(|el|
el.clone_node()
.unwrap()
.unchecked_into()
)
}
} else {
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]
.with(|el|
el.clone_node()
.unwrap()
.unchecked_into()
)
};
#[cfg(all(target_arch = "wasm32", feature = "web", not(feature = "hydrate")))]
let element = [<$tag:upper $(_ $second:upper $(_ $third: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 $($second:camel $($third:camel)?)?>] {
type Target = web_sys::Element;
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<web_sys::HtmlElement> for [<$tag:camel $($second:camel $($third:camel)?)?>] {
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 $($second:camel $($third:camel)?)?>] {
fn name(&self) -> Oco<'static, str> {
stringify!($tag).into()
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
fn hydration_id(&self) -> &Option<HydrationKey> {
&self.id
}
generate_math_tags! { @void $($void)? }
}
impl From<HtmlElement<[<$tag:camel $($second:camel $($third:camel)?)?>]>> for HtmlElement<AnyElement> {
fn from(element: HtmlElement<[<$tag:camel $($second:camel $($third:camel)?)?>]>) -> Self {
element.into_any()
}
}
#[$meta]
pub fn [<$tag $(_ $second $(_ $third)?)? $($trailing_)?>]() -> HtmlElement<[<$tag:camel $($second:camel $($third:camel)?)?>]> {
HtmlElement::new([<$tag:camel $($second:camel $($third:camel)?)?>]::default())
}
)*
}
};
(@void) => {};
(@void void) => {
fn is_void(&self) -> bool {
true
}
};
}
generate_math_tags![
/// MathML element.
math,
/// MathML element.
mi,
/// MathML element.
mn,
/// MathML element.
mo,
/// MathML element.
ms,
/// MathML element.
mspace,
/// MathML element.
mtext,
/// MathML element.
menclose,
/// MathML element.
merror,
/// MathML element.
mfenced,
/// MathML element.
mfrac,
/// MathML element.
mpadded,
/// MathML element.
mphantom,
/// MathML element.
mroot,
/// MathML element.
mrow,
/// MathML element.
msqrt,
/// MathML element.
mstyle,
/// MathML element.
mmultiscripts,
/// MathML element.
mover,
/// MathML element.
mprescripts,
/// MathML element.
msub,
/// MathML element.
msubsup,
/// MathML element.
msup,
/// MathML element.
munder,
/// MathML element.
munderover,
/// MathML element.
mtable,
/// MathML element.
mtd,
/// MathML element.
mtr,
/// MathML element.
maction,
/// MathML element.
annotation,
/// MathML element.
annotation
- xml,
/// MathML element.
semantics,
];

View file

@ -1,226 +0,0 @@
use crate::{html::ElementDescriptor, HtmlElement};
use leptos_reactive::{create_render_effect, signal_prelude::*};
use std::cell::Cell;
/// Contains a shared reference to a DOM node created while using the `view`
/// macro to create your UI.
///
/// ```
/// # use leptos::{*, logging::log};
/// use leptos::html::Input;
///
/// #[component]
/// pub fn MyComponent() -> impl IntoView {
/// let input_ref = create_node_ref::<Input>();
///
/// let on_click = move |_| {
/// let node =
/// input_ref.get().expect("input_ref should be loaded by now");
/// // `node` is strongly typed
/// // it is dereferenced to an `HtmlInputElement` automatically
/// log!("value is {:?}", node.value())
/// };
///
/// view! {
/// <div>
/// // `node_ref` loads the input
/// <input _ref=input_ref type="text"/>
/// // the button consumes it
/// <button on:click=on_click>"Click me"</button>
/// </div>
/// }
/// }
/// ```
#[cfg_attr(not(debug_assertions), repr(transparent))]
pub struct NodeRef<T: ElementDescriptor + 'static> {
element: RwSignal<Option<HtmlElement<T>>>,
#[cfg(debug_assertions)]
defined_at: &'static std::panic::Location<'static>,
}
/// Creates a shared reference to a DOM node created while using the `view`
/// macro to create your UI.
///
/// ```
/// # use leptos::{*, logging::log};
/// use leptos::html::Input;
///
/// #[component]
/// pub fn MyComponent() -> impl IntoView {
/// let input_ref = create_node_ref::<Input>();
///
/// let on_click = move |_| {
/// let node =
/// input_ref.get().expect("input_ref should be loaded by now");
/// // `node` is strongly typed
/// // it is dereferenced to an `HtmlInputElement` automatically
/// log!("value is {:?}", node.value())
/// };
///
/// view! {
/// <div>
/// // `node_ref` loads the input
/// <input _ref=input_ref type="text"/>
/// // the button consumes it
/// <button on:click=on_click>"Click me"</button>
/// </div>
/// }
/// }
/// ```
#[track_caller]
#[inline(always)]
pub fn create_node_ref<T: ElementDescriptor + 'static>() -> NodeRef<T> {
NodeRef {
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
element: create_rw_signal(None),
}
}
impl<T: ElementDescriptor + 'static> NodeRef<T> {
/// Creates a shared reference to a DOM node created while using the `view`
/// macro to create your UI.
///
/// This is identical to [`create_node_ref`].
///
/// ```
/// # use leptos::{*, logging::log};
///
/// use leptos::html::Input;
///
/// #[component]
/// pub fn MyComponent() -> impl IntoView {
/// let input_ref = NodeRef::<Input>::new();
///
/// let on_click = move |_| {
/// let node =
/// input_ref.get().expect("input_ref should be loaded by now");
/// // `node` is strongly typed
/// // it is dereferenced to an `HtmlInputElement` automatically
/// log!("value is {:?}", node.value())
/// };
///
/// view! {
/// <div>
/// // `node_ref` loads the input
/// <input _ref=input_ref type="text"/>
/// // the button consumes it
/// <button on:click=on_click>"Click me"</button>
/// </div>
/// }
/// }
/// ```
#[inline(always)]
#[track_caller]
pub fn new() -> Self {
create_node_ref()
}
/// Gets the element that is currently stored in the reference.
///
/// This tracks reactively, so that node references can be used in effects.
/// Initially, the value will be `None`, but once it is loaded the effect
/// will rerun and its value will be `Some(Element)`.
#[track_caller]
#[inline(always)]
pub fn get(&self) -> Option<HtmlElement<T>>
where
T: Clone,
{
self.element.get()
}
/// Gets the element that is currently stored in the reference.
///
/// This **does not** track reactively.
#[track_caller]
#[inline(always)]
pub fn get_untracked(&self) -> Option<HtmlElement<T>>
where
T: Clone,
{
self.element.get_untracked()
}
#[doc(hidden)]
/// Loads an element into the reference. This tracks reactively,
/// so that effects that use the node reference will rerun once it is loaded,
/// i.e., effects can be forward-declared.
#[track_caller]
pub fn load(&self, node: &HtmlElement<T>)
where
T: Clone,
{
self.element.update(|current| {
if current.is_some() {
#[cfg(debug_assertions)]
crate::debug_warn!(
"You are setting the NodeRef defined at {}, which has \
already been filled Its possible this is intentional, \
but its also possible that youre accidentally using \
the same NodeRef for multiple _ref attributes.",
self.defined_at
);
}
*current = Some(node.clone());
});
}
/// Runs the provided closure when the `NodeRef` has been connected
/// with it's [`HtmlElement`].
#[inline(always)]
pub fn on_load<F>(self, f: F)
where
T: Clone,
F: FnOnce(HtmlElement<T>) + 'static,
{
let f = Cell::new(Some(f));
create_render_effect(move |_| {
if let Some(node_ref) = self.get() {
f.take().unwrap()(node_ref);
}
});
}
}
impl<T: ElementDescriptor> Clone for NodeRef<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: ElementDescriptor + 'static> Copy for NodeRef<T> {}
impl<T: ElementDescriptor + 'static> Default for NodeRef<T> {
fn default() -> Self {
Self::new()
}
}
cfg_if::cfg_if! {
if #[cfg(feature = "nightly")] {
impl<T: Clone + ElementDescriptor + 'static> FnOnce<()> for NodeRef<T> {
type Output = Option<HtmlElement<T>>;
#[inline(always)]
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
self.get()
}
}
impl<T: Clone + ElementDescriptor + 'static> FnMut<()> for NodeRef<T> {
#[inline(always)]
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
self.get()
}
}
impl<T: Clone + ElementDescriptor + Clone + 'static> Fn<()> for NodeRef<T> {
#[inline(always)]
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
self.get()
}
}
}
}

View file

@ -1,148 +0,0 @@
use crate::{Attribute, IntoAttribute};
use leptos_reactive::use_context;
use std::{fmt::Display, ops::Deref};
/// A nonce a cryptographic nonce ("number used once") which can be
/// used by Content Security Policy to determine whether or not a given
/// resource will be allowed to load.
///
/// When the `nonce` feature is enabled on one of the server integrations,
/// a nonce is generated during server rendering and added to all inline
/// scripts used for HTML streaming and resource loading.
///
/// The nonce being used during the current server response can be
/// accessed using [`use_nonce`].
///
/// ```rust,ignore
/// #[component]
/// pub fn App() -> impl IntoView {
/// provide_meta_context;
///
/// view! {
/// // use `leptos_meta` to insert a <meta> tag with the CSP
/// <Meta
/// http_equiv="Content-Security-Policy"
/// content=move || {
/// // this will insert the CSP with nonce on the server, be empty on client
/// use_nonce()
/// .map(|nonce| {
/// format!(
/// "default-src 'self'; script-src 'strict-dynamic' 'nonce-{nonce}' \
/// 'wasm-unsafe-eval'; style-src 'nonce-{nonce}';"
/// )
/// })
/// .unwrap_or_default()
/// }
/// />
/// // manually insert nonce during SSR on inline script
/// <script nonce=use_nonce()>"console.log('Hello, world!');"</script>
/// // leptos_meta <Style/> and <Script/> automatically insert the nonce
/// <Style>"body { color: blue; }"</Style>
/// <p>"Test"</p>
/// }
/// }
/// ```
#[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<Self>) -> Attribute {
Attribute::String(self.0.into())
}
}
/// Accesses the nonce that has been generated during the current
/// server response. This can be added to inline `<script>` and
/// `<style>` tags for compatibility with a Content Security Policy.
///
/// ```rust,ignore
/// #[component]
/// pub fn App() -> impl IntoView {
/// provide_meta_context;
///
/// view! {
/// // use `leptos_meta` to insert a <meta> tag with the CSP
/// <Meta
/// http_equiv="Content-Security-Policy"
/// content=move || {
/// // this will insert the CSP with nonce on the server, be empty on client
/// use_nonce()
/// .map(|nonce| {
/// format!(
/// "default-src 'self'; script-src 'strict-dynamic' 'nonce-{nonce}' \
/// 'wasm-unsafe-eval'; style-src 'nonce-{nonce}';"
/// )
/// })
/// .unwrap_or_default()
/// }
/// />
/// // manually insert nonce during SSR on inline script
/// <script nonce=use_nonce()>"console.log('Hello, world!');"</script>
/// // leptos_meta <Style/> and <Script/> automatically insert the nonce
/// <Style>"body { color: blue; }"</Style>
/// <p>"Test"</p>
/// }
/// }
/// ```
pub fn use_nonce() -> Option<Nonce> {
use_context::<Nonce>()
}
#[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())
}
}

View file

@ -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<T> = Pin<Box<dyn Future<Output = T>>>;
/// 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! {
/// <p>"Hello, world!"</p>
/// });
/// // trim off the beginning, which has a bunch of hydration info, for comparison
/// assert!(html.contains("Hello, world!</p>"));
/// # }}
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn render_to_string<F, N>(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 `<Suspense/>`,
/// b) the `fallback` for any `<Suspense/>` 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 `<Suspense/>` fallback with its actual data as the resources
/// read under that `<Suspense/>` 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<Item = String> {
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 `<Suspense/>`,
/// b) the `fallback` for any `<Suspense/>` 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 `<Suspense/>` fallback with its actual data as the resources
/// read under that `<Suspense/>` 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<Item = String> {
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 `<Suspense/>`,
/// b) the `fallback` for any `<Suspense/>` 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 `<Suspense/>` fallback with its actual data as the resources
/// read under that `<Suspense/>` 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<Item = String>, 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 `<Suspense/>`,
/// b) the `fallback` for any `<Suspense/>` 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 `<Suspense/>` fallback with its actual data as the resources
/// read under that `<Suspense/>` 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<Item = String>, 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 `<Suspense/>`,
/// b) the `fallback` for any `<Suspense/>` 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 `<Suspense/>` fallback with its actual data as the resources
/// read under that `<Suspense/>` 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<Item = String>, 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<Box<dyn Future<Output = (String, String)>>>);
}
}
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!(
"<script{nonce_str}>__LEPTOS_PENDING_RESOURCES = \
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
Map();__LEPTOS_RESOURCE_RESOLVERS = new \
Map();__LEPTOS_LOCAL_ONLY = {local_only};</script>"
);
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!("<!--suspense-open-{blocked_id}-->");
let close =
format!("<!--suspense-close-{blocked_id}-->");
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<PinnedFuture<(String, String)>>,
serializers: FuturesUnordered<PinnedFuture<(ResourceId, String)>>,
) -> Pin<Box<dyn Stream<Item = String>>> {
// resources and fragments
// stream HTML for each <Suspense/> 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<dyn Future<Output = (String, String)>>>);
}
Box::pin(ooo_body_stream_recurse(
nonce_str,
fragments,
serializers,
))
as Pin<Box<dyn Stream<Item = String>>>
} 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<Item = (String, String)>,
) -> impl Stream<Item = String> {
fragments.map(move |(fragment_id, html)| {
format!(
r#"
<template id="{fragment_id}f">{html}</template>
<script{nonce_str}>
(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);}})()
</script>
"#
)
})
}
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 <Suspense/>` \
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!("<!--leptos-view|{id}|open-->{content}<!--leptos-view|{id}|close-->").into()
} else {
content.into()
}
} else {
format!(
r#"{}{}"#,
content(),
node.id.to_marker(true)
).into()
}
}
}
View::Suspense(id, node) => format!(
"<!--suspense-open-{id}-->{}<!--suspense-close-{id}-->",
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<dyn FnOnce() -> 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 <script> or <style>
let content = if dont_escape_text {
content
} else {
html_escape::encode_safe(&content)
.to_string()
.into()
};
// On debug builds, `DynChild` has two marker nodes,
// so there is no way for the text to be merged with
// surrounding text when the browser parses the HTML,
// but in release, `DynChild` only has a trailing marker,
// and the browser automatically merges the dynamic text
// into one single node, so we need to artificially make the
// browser create the dynamic text as it's own text node
if !cfg!(debug_assertions) {
format!("<!>{content}",).into()
} else {
content
}
} else {
child.render_to_string_helper(
dont_escape_text,
)
}
} else {
"".into()
}
})
as Box<dyn FnOnce() -> Oco<'static, str>>,
)
}
CoreComponent::Each(node) => {
let children = node.children.take();
(
node.id,
"each",
true,
Box::new(move || {
children
.into_iter()
.flatten()
.map(|node| {
let id = node.id;
let is_el = matches!(
node.child,
View::Element(_)
);
let content = || {
node.child.render_to_string_helper(
dont_escape_text,
)
};
if is_el {
content()
} else {
format!(
"{}{}{}",
id.to_marker(
false,
#[cfg(debug_assertions)]
"each-item",
),
content(),
id.to_marker(
true,
#[cfg(debug_assertions)]
"each-item",
)
)
.into()
}
})
.join("")
.into()
})
as Box<dyn FnOnce() -> Oco<'static, str>>,
)
}
};
if wrap {
format!(
r#"{}{}{}"#,
id.to_marker(
false,
#[cfg(debug_assertions)]
name,
),
content(),
id.to_marker(
true,
#[cfg(debug_assertions)]
name,
),
)
.into()
} else {
content()
}
}
View::Element(el) => {
let is_script_or_style =
el.name == "script" || el.name == "style";
let el_html = if let ElementChildren::Chunks(chunks) =
el.children
{
chunks
.into_iter()
.map(|chunk| match chunk {
StringOrView::String(string) => string,
StringOrView::View(view) => view()
.render_to_string_helper(is_script_or_style),
})
.join("")
.into()
} else {
let tag_name: Oco<'_, str> = el.name;
let mut inner_html: Option<Oco<'_, str>> = None;
let attrs = el
.attrs
.into_iter()
.filter_map(
|(name, value)| -> Option<Oco<'static, str>> {
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 {
format!("<{tag_name}{attrs}/>").into()
} else if let Some(inner_html) = inner_html {
format!("<{tag_name}{attrs}>{inner_html}</{tag_name}>")
.into()
} else {
let children = match el.children {
ElementChildren::Empty => "".into(),
ElementChildren::Children(c) => c
.into_iter()
.map(|v| {
v.render_to_string_helper(
is_script_or_style,
)
})
.join("")
.into(),
ElementChildren::InnerHtml(h) => h,
// already handled this case above
ElementChildren::Chunks(_) => unreachable!(),
};
format!("<{tag_name}{attrs}>{children}</{tag_name}>")
.into()
}
};
cfg_if! {
if #[cfg(debug_assertions)] {
if let Some(id) = el.view_marker {
format!("<!--leptos-view|{id}|open-->{el_html}<!--leptos-view|{id}|close-->").into()
} else {
el_html
}
} else {
el_html
}
}
}
View::Transparent(_) => Default::default(),
}
}
}
#[cfg(debug_assertions)]
pub(crate) fn to_kebab_case(name: &str) -> String {
if name.is_empty() {
return String::new();
}
let mut new_name = String::with_capacity(name.len() + 8);
let mut chars = name.chars();
new_name.push(
chars
.next()
.map(|mut c| {
if c.is_ascii() {
c.make_ascii_lowercase();
}
c
})
.unwrap(),
);
for mut char in chars {
if char.is_ascii_uppercase() {
char.make_ascii_lowercase();
new_name.push('-');
}
new_name.push(char);
}
new_name
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub(crate) fn render_serializers(
nonce_str: String,
serializers: FuturesUnordered<PinnedFuture<(ResourceId, String)>>,
) -> impl Stream<Item = String> {
serializers.map(move |(id, json)| {
let id = serde_json::to_string(&id).unwrap();
let json = json.replace('<', "\\u003c");
format!(
r#"<script{nonce_str}>
(function() {{ let val = {json:?};
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
__LEPTOS_RESOURCE_RESOLVERS.get({id})(val)
}} else {{
__LEPTOS_RESOLVED_RESOURCES.set({id}, val);
}} }})();
</script>"#,
)
})
}
#[doc(hidden)]
pub fn escape_attr<T>(value: &T) -> Oco<'_, str>
where
T: AsRef<str>,
{
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!("<!--hk={self}c|leptos-{component_name}-end-->").into()
} else {
format!("<!--hk={self}o|leptos-{component_name}-start-->")
.into()
}
}
#[cfg(not(debug_assertions))]
{
if closing {
format!("<!--hk={self}-->").into()
} else {
"".into()
}
}
}
}
impl ToMarker for Option<HydrationKey> {
#[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())
}
}

View file

@ -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 `<Suspense/>` 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 `<Suspense/>` components. The stream contains,
/// in order:
/// 1. HTML from the `view` in order, pausing to wait for each `<Suspense/>`
/// 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<Item = String> {
render_to_stream_in_order_with_prefix(view, || "".into())
}
/// Renders an in-order HTML stream, pausing at `<Suspense/>` components. The stream contains,
/// in order:
/// 1. `prefix`
/// 2. HTML from the `view` in order, pausing to wait for each `<Suspense/>`
/// 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 `<Suspense/>` 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<Item = String> {
#[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 <Suspense/>` \
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 `<Suspense/>` components. The stream contains,
/// in order:
/// 1. `prefix`
/// 2. HTML from the `view` in order, pausing to wait for each `<Suspense/>`
/// 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 `<Suspense/>` 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<Item = String>, 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}
<script{nonce_str}>
__LEPTOS_PENDING_RESOURCES = {pending_resources};
__LEPTOS_RESOLVED_RESOURCES = new Map();
__LEPTOS_RESOURCE_RESOLVERS = new Map();
__LEPTOS_LOCAL_ONLY = {local_only};
</script>
"#
)
}
})
.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<String>,
mut queued_chunks: VecDeque<StreamChunk>,
) -> VecDeque<StreamChunk> {
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<String>,
chunks: VecDeque<StreamChunk>,
) {
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<StreamChunk> {
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<StreamChunk>,
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!("<!--leptos-view|{id}|open-->").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<Oco<'static, str>> {
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}</{tag_name}>"
)
.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!("</{tag_name}>").into(),
));
}
}
#[cfg(debug_assertions)]
if let Some(id) = &el.view_marker {
chunks.push_back(StreamChunk::Sync(
format!("<!--leptos-view|{id}|close-->").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<StreamChunk>| {
chunks.push_back(StreamChunk::Sync(
u.id.to_marker(
true,
#[cfg(debug_assertions)]
"unit",
),
));
})
as Box<dyn FnOnce(&mut VecDeque<StreamChunk>)>,
),
CoreComponent::DynChild(node) => {
let child = node.child.take();
(
node.id,
"dyn-child",
true,
Box::new(
move |chunks: &mut VecDeque<StreamChunk>| {
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 <script> or <style>
let content = if dont_escape_text {
content
} else {
html_escape::encode_safe(
&content,
)
.to_string()
.into()
};
// On debug builds, `DynChild` has two marker nodes,
// so there is no way for the text to be merged with
// surrounding text when the browser parses the HTML,
// but in release, `DynChild` only has a trailing marker,
// and the browser automatically merges the dynamic text
// into one single node, so we need to artificially make the
// browser create the dynamic text as it's own text node
chunks.push_back(
if !cfg!(debug_assertions) {
StreamChunk::Sync(
format!(
"<!>{}",
html_escape::encode_safe(
&content
)
)
.into(),
)
} else {
StreamChunk::Sync(html_escape::encode_safe(
&content
).to_string().into())
},
);
} else {
child.into_stream_chunks_helper(
chunks,
dont_escape_text,
);
}
}
},
)
as Box<dyn FnOnce(&mut VecDeque<StreamChunk>)>,
)
}
CoreComponent::Each(node) => {
let children = node.children.take();
(
node.id,
"each",
true,
Box::new(
move |chunks: &mut VecDeque<StreamChunk>| {
for node in children.into_iter().flatten() {
let id = node.id;
let is_el = matches!(
node.child,
View::Element(_)
);
#[cfg(debug_assertions)]
if !is_el {
chunks.push_back(StreamChunk::Sync(
id.to_marker(
false,
"each-item",
),
))
};
node.child.into_stream_chunks_helper(
chunks,
dont_escape_text,
);
if !is_el {
chunks.push_back(
StreamChunk::Sync(
id.to_marker(
true,
#[cfg(
debug_assertions
)]
"each-item",
),
),
);
}
}
},
)
as Box<dyn FnOnce(&mut VecDeque<StreamChunk>)>,
)
}
};
if wrap {
#[cfg(debug_assertions)]
{
chunks.push_back(StreamChunk::Sync(
id.to_marker(false, name),
));
}
content(chunks);
chunks.push_back(StreamChunk::Sync(id.to_marker(
true,
#[cfg(debug_assertions)]
name,
)));
} else {
content(chunks);
}
}
}
}
}

View file

@ -1,302 +0,0 @@
//! Exports types for working with SVG elements.
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
use super::{html::HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG, HydrationKey};
use super::{AnyElement, ElementDescriptor, HtmlElement};
use crate::HydrationCtx;
use leptos_reactive::Oco;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use once_cell::unsync::Lazy as LazyCell;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use wasm_bindgen::JsCast;
macro_rules! generate_svg_tags {
(
$(
#[$meta:meta]
$(#[$void:ident])?
$tag:ident $(- $second:ident $(- $third:ident)?)? $(@ $trailing_:pat)?
),* $(,)?
) => {
paste::paste! {
$(
#[cfg(all(target_arch = "wasm32", feature = "web"))]
thread_local! {
static [<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]: LazyCell<web_sys::HtmlElement> = LazyCell::new(|| {
crate::document()
.create_element_ns(
Some(wasm_bindgen::intern("http://www.w3.org/2000/svg")),
concat![
stringify!($tag),
$(
"-", stringify!($second),
$(
"-", stringify!($third)
)?
)?
],
)
.unwrap()
.unchecked_into()
});
}
#[derive(Clone, Debug)]
#[$meta]
pub struct [<$tag:camel $($second:camel $($third:camel)?)?>] {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
element: web_sys::HtmlElement,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id: Option<HydrationKey>,
}
impl Default for [<$tag:camel $($second:camel $($third:camel)?)?>] {
fn default() -> Self {
#[allow(unused)]
let id = HydrationCtx::id();
#[cfg(all(target_arch = "wasm32", feature = "hydrate"))]
let element = 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(),
stringify!([<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]),
"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 {
crate::warn!(
"element with id {id} not found, ignoring it for hydration"
);
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]
.with(|el|
el.clone_node()
.unwrap()
.unchecked_into()
)
}
} else {
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]
.with(|el|
el.clone_node()
.unwrap()
.unchecked_into()
)
};
#[cfg(all(target_arch = "wasm32", feature = "web", not(feature = "hydrate")))]
let element = [<$tag:upper $(_ $second:upper $(_ $third: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 $($second:camel $($third:camel)?)?>] {
type Target = web_sys::SvgElement;
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<web_sys::HtmlElement> for [<$tag:camel $($second:camel $($third:camel)?)?>] {
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 $($second:camel $($third:camel)?)?>] {
fn name(&self) -> Oco<'static, str> {
stringify!($tag).into()
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
fn hydration_id(&self) -> &Option<HydrationKey> {
&self.id
}
generate_svg_tags! { @void $($void)? }
}
impl From<HtmlElement<[<$tag:camel $($second:camel $($third:camel)?)?>]>> for HtmlElement<AnyElement> {
fn from(element: HtmlElement<[<$tag:camel $($second:camel $($third:camel)?)?>]>) -> Self {
element.into_any()
}
}
#[$meta]
#[allow(non_snake_case)]
pub fn [<$tag $(_ $second $(_ $third)?)? $($trailing_)?>]() -> HtmlElement<[<$tag:camel $($second:camel $($third:camel)?)?>]> {
HtmlElement::new([<$tag:camel $($second:camel $($third:camel)?)?>]::default())
}
)*
}
};
(@void) => {};
(@void void) => {
fn is_void(&self) -> bool {
true
}
};
}
generate_svg_tags![
/// SVG Element.
a,
/// SVG Element.
animate,
/// SVG Element.
animateMotion,
/// SVG Element.
animateTransform,
/// SVG Element.
circle,
/// SVG Element.
clipPath,
/// SVG Element.
defs,
/// SVG Element.
desc,
/// SVG Element.
discard,
/// SVG Element.
ellipse,
/// SVG Element.
feBlend,
/// SVG Element.
feColorMatrix,
/// SVG Element.
feComponentTransfer,
/// SVG Element.
feComposite,
/// SVG Element.
feConvolveMatrix,
/// SVG Element.
feDiffuseLighting,
/// SVG Element.
feDisplacementMap,
/// SVG Element.
feDistantLight,
/// SVG Element.
feDropShadow,
/// SVG Element.
feFlood,
/// SVG Element.
feFuncA,
/// SVG Element.
feFuncB,
/// SVG Element.
feFuncG,
/// SVG Element.
feFuncR,
/// SVG Element.
feGaussianBlur,
/// SVG Element.
feImage,
/// SVG Element.
feMerge,
/// SVG Element.
feMergeNode,
/// SVG Element.
feMorphology,
/// SVG Element.
feOffset,
/// SVG Element.
fePointLight,
/// SVG Element.
feSpecularLighting,
/// SVG Element.
feSpotLight,
/// SVG Element.
feTile,
/// SVG Element.
feTurbulence,
/// SVG Element.
filter,
/// SVG Element.
foreignObject,
/// SVG Element.
g,
/// SVG Element.
hatch,
/// SVG Element.
hatchpath,
/// SVG Element.
image,
/// SVG Element.
line,
/// SVG Element.
linearGradient,
/// SVG Element.
marker,
/// SVG Element.
mask,
/// SVG Element.
metadata,
/// SVG Element.
mpath,
/// SVG Element.
path,
/// SVG Element.
pattern,
/// SVG Element.
polygon,
/// SVG Element.
polyline,
/// SVG Element.
radialGradient,
/// SVG Element.
rect,
/// SVG Element.
script,
/// SVG Element.
set,
/// SVG Element.
stop,
/// SVG Element.
style,
/// SVG Element.
svg,
/// SVG Element.
switch,
/// SVG Element.
symbol,
/// SVG Element.
text,
/// SVG Element.
textPath,
/// SVG Element.
title,
/// SVG Element.
tspan,
/// SVG Element.
use @_,
/// SVG Element.
view,
];

View file

@ -1,49 +0,0 @@
use crate::{IntoView, View};
use std::{any::Any, fmt, rc::Rc};
/// Wrapper for arbitrary data that can be passed through the view.
#[derive(Clone)]
#[repr(transparent)]
pub struct Transparent(Rc<dyn Any>);
impl Transparent {
/// Creates a new wrapper for this data.
#[inline(always)]
pub fn new<T>(value: T) -> Self
where
T: 'static,
{
Self(Rc::new(value))
}
/// Returns some reference to the inner value if it is of type `T`, or `None` if it isn't.
#[inline(always)]
pub fn downcast_ref<T>(&self) -> Option<&T>
where
T: 'static,
{
self.0.downcast_ref()
}
}
impl fmt::Debug for Transparent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Transparent").finish()
}
}
impl PartialEq for Transparent {
#[inline(always)]
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(&self.0, &other.0)
}
}
impl Eq for Transparent {}
impl IntoView for Transparent {
#[inline(always)]
fn into_view(self) -> View {
View::Transparent(self)
}
}