mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 07:34:35 -05:00
1300 lines
36 KiB
Rust
1300 lines
36 KiB
Rust
#![deny(missing_docs)]
|
|
#![forbid(unsafe_code)]
|
|
#![cfg_attr(feature = "nightly", feature(fn_traits))]
|
|
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
|
|
// to prevent warnings from popping up when a nightly feature is stabilized
|
|
#![allow(stable_features)]
|
|
|
|
//! The DOM implementation for `leptos`.
|
|
|
|
#[doc(hidden)]
|
|
#[cfg_attr(any(debug_assertions, feature = "ssr"), macro_use)]
|
|
pub extern crate tracing;
|
|
|
|
mod components;
|
|
mod directive;
|
|
mod events;
|
|
pub mod helpers;
|
|
pub mod html;
|
|
mod hydration;
|
|
/// Utilities for simple isomorphic logging to the console or terminal.
|
|
pub mod logging;
|
|
mod macro_helpers;
|
|
pub mod math;
|
|
mod node_ref;
|
|
/// Utilities for exporting nonces to be used for a Content Security Policy.
|
|
pub mod nonce;
|
|
pub mod ssr;
|
|
pub mod ssr_in_order;
|
|
pub mod svg;
|
|
mod transparent;
|
|
|
|
use cfg_if::cfg_if;
|
|
pub use components::*;
|
|
pub use directive::*;
|
|
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
|
pub use events::add_event_helper;
|
|
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
|
use events::{add_event_listener, add_event_listener_undelegated};
|
|
pub use events::{
|
|
typed as ev,
|
|
typed::{EventHandler, EventHandlerFn},
|
|
};
|
|
pub use html::HtmlElement;
|
|
use html::{AnyElement, ElementDescriptor};
|
|
pub use hydration::{HydrationCtx, HydrationKey};
|
|
#[cfg(not(feature = "nightly"))]
|
|
use leptos_reactive::{
|
|
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
|
|
};
|
|
use leptos_reactive::{Oco, TextProp};
|
|
pub use macro_helpers::*;
|
|
pub use node_ref::*;
|
|
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
|
use once_cell::unsync::Lazy as LazyCell;
|
|
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
|
use smallvec::SmallVec;
|
|
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
|
use std::cell::RefCell;
|
|
use std::{borrow::Cow, fmt, rc::Rc};
|
|
pub use transparent::*;
|
|
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
|
use wasm_bindgen::JsCast;
|
|
use wasm_bindgen::UnwrapThrowExt;
|
|
|
|
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
|
thread_local! {
|
|
static COMMENT: LazyCell<web_sys::Node> =
|
|
LazyCell::new(|| document().create_comment("").unchecked_into());
|
|
static RANGE: LazyCell<web_sys::Range> =
|
|
LazyCell::new(|| web_sys::Range::new().unwrap());
|
|
}
|
|
|
|
/// Converts the value into a [`View`].
|
|
pub trait IntoView {
|
|
/// Converts the value into [`View`].
|
|
fn into_view(self) -> View;
|
|
}
|
|
|
|
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
|
#[doc(hidden)]
|
|
pub trait Mountable {
|
|
/// Gets the [`web_sys::Node`] that can be directly inserted as
|
|
/// a child of another node. Typically, this is a [`web_sys::DocumentFragment`]
|
|
/// for components, and [`web_sys::HtmlElement`] for elements.
|
|
///
|
|
/// ### Important Note
|
|
/// Calling this method assumes that you are intending to move this
|
|
/// view, and will unmount it's nodes from the DOM if this view is a
|
|
/// component. In other words, don't call this method unless you intend
|
|
/// to mount this view to another view or element.
|
|
fn get_mountable_node(&self) -> web_sys::Node;
|
|
|
|
/// Get's the first node of the [`View`].
|
|
/// Typically, for [`HtmlElement`], this will be the
|
|
/// `element` node. For components, this would be the
|
|
/// first child node, or the `closing` marker comment node if
|
|
/// no children are available.
|
|
fn get_opening_node(&self) -> web_sys::Node;
|
|
|
|
/// Get's the closing marker node.
|
|
fn get_closing_node(&self) -> web_sys::Node;
|
|
}
|
|
|
|
impl IntoView for () {
|
|
#[cfg_attr(
|
|
any(debug_assertions, feature = "ssr"),
|
|
instrument(level = "trace", name = "<() />", skip_all)
|
|
)]
|
|
fn into_view(self) -> View {
|
|
Unit.into_view()
|
|
}
|
|
}
|
|
|
|
impl<T> IntoView for Option<T>
|
|
where
|
|
T: IntoView,
|
|
{
|
|
#[cfg_attr(
|
|
any(debug_assertions, feature = "ssr"),
|
|
instrument(level = "trace", name = "Option<T>", skip_all)
|
|
)]
|
|
fn into_view(self) -> View {
|
|
if let Some(t) = self {
|
|
t.into_view()
|
|
} else {
|
|
Unit.into_view()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<F, N> IntoView for F
|
|
where
|
|
F: Fn() -> N + 'static,
|
|
N: IntoView,
|
|
{
|
|
#[cfg_attr(
|
|
any(debug_assertions, feature = "ssr"),
|
|
instrument(level = "trace", name = "Fn() -> impl IntoView", skip_all)
|
|
)]
|
|
#[track_caller]
|
|
fn into_view(self) -> View {
|
|
DynChild::new(self).into_view()
|
|
}
|
|
}
|
|
|
|
impl<N> IntoView for Rc<dyn Fn() -> N>
|
|
where
|
|
N: IntoView + 'static,
|
|
{
|
|
#[inline]
|
|
fn into_view(self) -> View {
|
|
// reuse impl for `Fn() -> impl IntoView`
|
|
IntoView::into_view(move || self())
|
|
}
|
|
}
|
|
|
|
#[cfg(not(feature = "nightly"))]
|
|
impl<T> IntoView for ReadSignal<T>
|
|
where
|
|
T: IntoView + Clone,
|
|
{
|
|
#[cfg_attr(
|
|
any(debug_assertions, feature = "ssr"),
|
|
instrument(level = "trace", name = "ReadSignal<T>", skip_all)
|
|
)]
|
|
fn into_view(self) -> View {
|
|
DynChild::new(move || self.get()).into_view()
|
|
}
|
|
}
|
|
#[cfg(not(feature = "nightly"))]
|
|
impl<T> IntoView for RwSignal<T>
|
|
where
|
|
T: IntoView + Clone,
|
|
{
|
|
#[cfg_attr(
|
|
any(debug_assertions, feature = "ssr"),
|
|
instrument(level = "trace", name = "RwSignal<T>", skip_all)
|
|
)]
|
|
fn into_view(self) -> View {
|
|
DynChild::new(move || self.get()).into_view()
|
|
}
|
|
}
|
|
#[cfg(not(feature = "nightly"))]
|
|
impl<T> IntoView for Memo<T>
|
|
where
|
|
T: IntoView + Clone,
|
|
{
|
|
#[cfg_attr(
|
|
any(debug_assertions, feature = "ssr"),
|
|
instrument(level = "trace", name = "Memo<T>", skip_all)
|
|
)]
|
|
fn into_view(self) -> View {
|
|
DynChild::new(move || self.get()).into_view()
|
|
}
|
|
}
|
|
#[cfg(not(feature = "nightly"))]
|
|
impl<T> IntoView for Signal<T>
|
|
where
|
|
T: IntoView + Clone,
|
|
{
|
|
#[cfg_attr(
|
|
any(debug_assertions, feature = "ssr"),
|
|
instrument(level = "trace", name = "Signal<T>", skip_all)
|
|
)]
|
|
fn into_view(self) -> View {
|
|
DynChild::new(move || self.get()).into_view()
|
|
}
|
|
}
|
|
#[cfg(not(feature = "nightly"))]
|
|
impl<T> IntoView for MaybeSignal<T>
|
|
where
|
|
T: IntoView + Clone,
|
|
{
|
|
#[cfg_attr(
|
|
any(debug_assertions, feature = "ssr"),
|
|
instrument(level = "trace", name = "MaybeSignal<T>", skip_all)
|
|
)]
|
|
fn into_view(self) -> View {
|
|
DynChild::new(move || self.get()).into_view()
|
|
}
|
|
}
|
|
|
|
#[cfg(not(feature = "nightly"))]
|
|
impl<T> IntoView for MaybeProp<T>
|
|
where
|
|
T: IntoView + Clone,
|
|
{
|
|
#[cfg_attr(
|
|
any(debug_assertions, feature = "ssr"),
|
|
instrument(level = "trace", name = "MaybeSignal<T>", skip_all)
|
|
)]
|
|
fn into_view(self) -> View {
|
|
DynChild::new(move || self.get()).into_view()
|
|
}
|
|
}
|
|
|
|
impl IntoView for TextProp {
|
|
fn into_view(self) -> View {
|
|
self.get().into_view()
|
|
}
|
|
}
|
|
|
|
/// Collects an iterator or collection into a [`View`].
|
|
pub trait CollectView {
|
|
/// Collects an iterator or collection into a [`View`].
|
|
fn collect_view(self) -> View;
|
|
}
|
|
|
|
impl<I: IntoIterator<Item = T>, T: IntoView> CollectView for I {
|
|
#[cfg_attr(
|
|
any(debug_assertions, feature = "ssr"),
|
|
instrument(level = "trace", name = "#text", skip_all)
|
|
)]
|
|
fn collect_view(self) -> View {
|
|
self.into_iter()
|
|
.map(|v| v.into_view())
|
|
.collect::<Fragment>()
|
|
.into_view()
|
|
}
|
|
}
|
|
|
|
cfg_if! {
|
|
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
|
/// HTML element.
|
|
#[derive(Clone, PartialEq, Eq)]
|
|
pub struct Element {
|
|
#[doc(hidden)]
|
|
#[cfg(debug_assertions)]
|
|
pub name: Oco<'static, str>,
|
|
#[doc(hidden)]
|
|
pub element: web_sys::HtmlElement,
|
|
#[cfg(debug_assertions)]
|
|
/// Optional marker for the view macro source of the element.
|
|
pub view_marker: Option<String>
|
|
}
|
|
|
|
impl fmt::Debug for Element {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let html = self.element.outer_html();
|
|
|
|
f.write_str(&html)
|
|
}
|
|
}
|
|
} else {
|
|
use crate::html::ElementChildren;
|
|
|
|
/// HTML element.
|
|
#[derive(Clone, PartialEq, Eq)]
|
|
pub struct Element {
|
|
name: Oco<'static, str>,
|
|
is_void: bool,
|
|
attrs: SmallVec<[(Oco<'static, str>, Oco<'static, str>); 4]>,
|
|
children: ElementChildren,
|
|
id: Option<HydrationKey>,
|
|
#[cfg(debug_assertions)]
|
|
/// Optional marker for the view macro source, in debug mode.
|
|
pub view_marker: Option<String>
|
|
}
|
|
|
|
impl fmt::Debug for Element {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
use fmt::Write;
|
|
|
|
let attrs = self.attrs.iter().fold(String::new(), |mut output, (n, v)| {
|
|
// can safely ignore output
|
|
// see https://rust-lang.github.io/rust-clippy/master/index.html#/format_collect
|
|
let _ = write!(output, " {n}=\"{v}\"");
|
|
output
|
|
});
|
|
|
|
if self.is_void {
|
|
write!(f, "<{}{attrs} />", self.name)
|
|
} else {
|
|
writeln!(f, "<{}{attrs}>", self.name)?;
|
|
|
|
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
|
|
|
|
if let ElementChildren::Children(children) = &self.children {
|
|
for child in children {
|
|
writeln!(pad_adapter, "{child:#?}")?;
|
|
}
|
|
}
|
|
|
|
write!(f, "</{}>", self.name)
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Element {
|
|
/// Converts this leptos [`Element`] into [`HtmlElement<AnyElement>`].
|
|
pub fn into_html_element(self) -> HtmlElement<AnyElement> {
|
|
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
|
{
|
|
let Self {
|
|
element,
|
|
#[cfg(debug_assertions)]
|
|
view_marker,
|
|
..
|
|
} = self;
|
|
|
|
let name = element.node_name().to_ascii_lowercase();
|
|
|
|
let element = AnyElement {
|
|
name: name.into(),
|
|
element,
|
|
is_void: false,
|
|
};
|
|
|
|
HtmlElement {
|
|
element,
|
|
#[cfg(debug_assertions)]
|
|
span: ::tracing::Span::current(),
|
|
#[cfg(debug_assertions)]
|
|
view_marker,
|
|
}
|
|
}
|
|
|
|
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
|
{
|
|
let Self {
|
|
name,
|
|
is_void,
|
|
attrs,
|
|
children,
|
|
id,
|
|
#[cfg(debug_assertions)]
|
|
view_marker,
|
|
} = self;
|
|
|
|
let element = AnyElement { name, is_void, id };
|
|
|
|
HtmlElement {
|
|
element,
|
|
attrs,
|
|
children,
|
|
#[cfg(debug_assertions)]
|
|
view_marker,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl IntoView for Element {
|
|
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "<Element />", skip_all, fields(tag = %self.name)))]
|
|
fn into_view(self) -> View {
|
|
View::Element(self)
|
|
}
|
|
}
|
|
|
|
impl Element {
|
|
#[track_caller]
|
|
fn new<El: ElementDescriptor>(el: El) -> Self {
|
|
cfg_if! {
|
|
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
|
Self {
|
|
#[cfg(debug_assertions)]
|
|
name: el.name(),
|
|
element: el.as_ref().clone(),
|
|
#[cfg(debug_assertions)]
|
|
view_marker: None
|
|
}
|
|
}
|
|
else {
|
|
Self {
|
|
name: el.name(),
|
|
is_void: el.is_void(),
|
|
attrs: Default::default(),
|
|
children: Default::default(),
|
|
id: *el.hydration_id(),
|
|
#[cfg(debug_assertions)]
|
|
view_marker: None
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
struct Comment {
|
|
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
|
node: web_sys::Node,
|
|
content: Oco<'static, str>,
|
|
}
|
|
|
|
impl Comment {
|
|
#[inline]
|
|
fn new(
|
|
content: impl Into<Oco<'static, str>>,
|
|
id: &Option<HydrationKey>,
|
|
closing: bool,
|
|
) -> Self {
|
|
Self::new_inner(content.into(), id, closing)
|
|
}
|
|
|
|
fn new_inner(
|
|
content: Oco<'static, str>,
|
|
id: &Option<HydrationKey>,
|
|
closing: bool,
|
|
) -> Self {
|
|
cfg_if! {
|
|
if #[cfg(not(all(target_arch = "wasm32", feature = "web")))] {
|
|
let _ = id;
|
|
let _ = closing;
|
|
|
|
Self { content }
|
|
} else {
|
|
#[cfg(not(feature = "hydrate"))]
|
|
{
|
|
_ = id;
|
|
_ = closing;
|
|
}
|
|
|
|
let node = COMMENT.with(|comment| comment.clone_node().unwrap());
|
|
|
|
#[cfg(debug_assertions)]
|
|
node.set_text_content(Some(&format!(" {content} ")));
|
|
|
|
#[cfg(feature = "hydrate")]
|
|
if HydrationCtx::is_hydrating() && id.is_some() {
|
|
let id = id.as_ref().unwrap();
|
|
let id = HydrationCtx::to_string(id, closing);
|
|
|
|
if let Some(marker) = hydration::get_marker(&id) {
|
|
marker.before_with_node_1(&node).unwrap();
|
|
|
|
marker.remove();
|
|
} else {
|
|
crate::warn!(
|
|
"component with id {id} not found, ignoring it for \
|
|
hydration"
|
|
);
|
|
}
|
|
}
|
|
|
|
Self {
|
|
node,
|
|
content,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// HTML text
|
|
#[derive(Clone, PartialEq, Eq)]
|
|
pub struct Text {
|
|
/// In order to support partial updates on text nodes, that is,
|
|
/// to update the node without recreating it, we need to be able
|
|
/// to possibly reuse a previous node.
|
|
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
|
pub(crate) node: web_sys::Node,
|
|
/// The current contents of the text node.
|
|
pub content: Oco<'static, str>,
|
|
}
|
|
|
|
impl fmt::Debug for Text {
|
|
#[inline]
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
fmt::Debug::fmt(&self.content, f)
|
|
}
|
|
}
|
|
|
|
impl IntoView for Text {
|
|
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "#text", skip_all, fields(content = %self.content)))]
|
|
fn into_view(self) -> View {
|
|
View::Text(self)
|
|
}
|
|
}
|
|
|
|
impl Text {
|
|
/// Creates a new [`Text`].
|
|
pub fn new(content: Oco<'static, str>) -> Self {
|
|
Self {
|
|
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
|
node: crate::document()
|
|
.create_text_node(&content)
|
|
.unchecked_into::<web_sys::Node>(),
|
|
content,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A leptos view which can be mounted to the DOM.
|
|
#[derive(Clone, PartialEq, Eq)]
|
|
#[must_use = "You are creating a View 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."]
|
|
pub enum View {
|
|
/// HTML element node.
|
|
Element(Element),
|
|
/// HTML text node.
|
|
Text(Text),
|
|
/// Custom leptos component.
|
|
Component(ComponentRepr),
|
|
/// leptos core-component.
|
|
CoreComponent(CoreComponent),
|
|
/// Wraps arbitrary data that's not part of the view but is
|
|
/// passed via the view tree.
|
|
Transparent(Transparent),
|
|
/// Marks the contents of Suspense component, which can be replaced in streaming SSR.
|
|
Suspense(HydrationKey, CoreComponent),
|
|
}
|
|
|
|
impl fmt::Debug for View {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Element(el) => el.fmt(f),
|
|
Self::Text(t) => t.fmt(f),
|
|
Self::Component(c) => c.fmt(f),
|
|
Self::CoreComponent(c) => c.fmt(f),
|
|
Self::Transparent(arg0) => {
|
|
f.debug_tuple("Transparent").field(arg0).finish()
|
|
}
|
|
Self::Suspense(id, c) => {
|
|
f.debug_tuple("Suspense").field(id).field(c).finish()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The default [`View`] is the [`Unit`] core-component.
|
|
impl Default for View {
|
|
fn default() -> Self {
|
|
Self::CoreComponent(Default::default())
|
|
}
|
|
}
|
|
|
|
impl IntoView for View {
|
|
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "Node", skip_all, fields(kind = self.kind_name())))]
|
|
fn into_view(self) -> View {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl IntoView for &View {
|
|
fn into_view(self) -> View {
|
|
self.clone()
|
|
}
|
|
}
|
|
|
|
impl<const N: usize> IntoView for [View; N] {
|
|
#[cfg_attr(
|
|
any(debug_assertions, feature = "ssr"),
|
|
instrument(level = "trace", name = "[Node; N]", skip_all)
|
|
)]
|
|
fn into_view(self) -> View {
|
|
Fragment::new(self.into_iter().collect()).into_view()
|
|
}
|
|
}
|
|
|
|
impl IntoView for &Fragment {
|
|
fn into_view(self) -> View {
|
|
self.to_owned().into_view()
|
|
}
|
|
}
|
|
|
|
impl FromIterator<View> for View {
|
|
fn from_iter<T: IntoIterator<Item = View>>(iter: T) -> Self {
|
|
iter.into_iter().collect::<Fragment>().into()
|
|
}
|
|
}
|
|
|
|
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
|
impl Mountable for View {
|
|
fn get_mountable_node(&self) -> web_sys::Node {
|
|
match self {
|
|
Self::Element(element) => {
|
|
element.element.unchecked_ref::<web_sys::Node>().clone()
|
|
}
|
|
Self::Text(t) => t.node.clone(),
|
|
Self::CoreComponent(c) | Self::Suspense(_, c) => match c {
|
|
CoreComponent::Unit(u) => u.get_mountable_node(),
|
|
CoreComponent::DynChild(dc) => dc.get_mountable_node(),
|
|
CoreComponent::Each(e) => e.get_mountable_node(),
|
|
},
|
|
Self::Component(c) => c.get_mountable_node(),
|
|
Self::Transparent(_) => {
|
|
panic!("tried to mount a Transparent node.")
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_opening_node(&self) -> web_sys::Node {
|
|
match self {
|
|
Self::Text(t) => t.node.clone(),
|
|
Self::Element(el) => el.element.clone().unchecked_into(),
|
|
Self::CoreComponent(c) | Self::Suspense(_, c) => match c {
|
|
CoreComponent::DynChild(dc) => dc.get_opening_node(),
|
|
CoreComponent::Each(e) => e.get_opening_node(),
|
|
CoreComponent::Unit(u) => u.get_opening_node(),
|
|
},
|
|
Self::Component(c) => c.get_opening_node(),
|
|
Self::Transparent(_) => {
|
|
panic!("tried to get opening node for a Transparent node.")
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_closing_node(&self) -> web_sys::Node {
|
|
match self {
|
|
Self::Text(t) => t.node.clone(),
|
|
Self::Element(el) => el.element.clone().unchecked_into(),
|
|
Self::CoreComponent(c) | Self::Suspense(_, c) => match c {
|
|
CoreComponent::DynChild(dc) => dc.get_closing_node(),
|
|
CoreComponent::Each(e) => e.get_closing_node(),
|
|
CoreComponent::Unit(u) => u.get_closing_node(),
|
|
},
|
|
Self::Component(c) => c.get_closing_node(),
|
|
Self::Transparent(_) => {
|
|
panic!("tried to get closing node for a Transparent node.")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl View {
|
|
#[cfg(debug_assertions)]
|
|
fn kind_name(&self) -> &'static str {
|
|
match self {
|
|
Self::Component(..) => "Component",
|
|
Self::Element(..) => "Element",
|
|
Self::Text(..) => "Text",
|
|
Self::CoreComponent(c) => match c {
|
|
CoreComponent::DynChild(..) => "DynChild",
|
|
CoreComponent::Each(..) => "Each",
|
|
CoreComponent::Unit(..) => "Unit",
|
|
},
|
|
Self::Transparent(..) => "Transparent",
|
|
Self::Suspense(..) => "Suspense",
|
|
}
|
|
}
|
|
|
|
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
|
fn get_text(&self) -> Option<&Text> {
|
|
if let Self::Text(t) = self {
|
|
Some(t)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Returns [`Some`] [`Text`] if the view is of this type. [`None`]
|
|
/// otherwise.
|
|
pub fn as_text(&self) -> Option<&Text> {
|
|
if let Self::Text(t) = self {
|
|
Some(t)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Returns [`Some`] [`Element`] if the view is of this type. [`None`]
|
|
/// otherwise.
|
|
pub fn as_element(&self) -> Option<&Element> {
|
|
if let Self::Element(el) = self {
|
|
Some(el)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Returns [`Some`] [`Transparent`] if the view is of this type. [`None`]
|
|
/// otherwise.
|
|
pub fn as_transparent(&self) -> Option<&Transparent> {
|
|
match &self {
|
|
Self::Transparent(t) => Some(t),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Returns [`Ok(HtmlElement<AnyElement>)`] if this [`View`] is
|
|
/// of type [`Element`]. [`Err(View)`] otherwise.
|
|
pub fn into_html_element(self) -> Result<HtmlElement<AnyElement>, Self> {
|
|
if let Self::Element(el) = self {
|
|
Ok(el.into_html_element())
|
|
} else {
|
|
Err(self)
|
|
}
|
|
}
|
|
|
|
/// Adds an event listener, analogous to [`HtmlElement::on`].
|
|
///
|
|
/// This method will attach an event listener to **all** children
|
|
#[inline(always)]
|
|
pub fn on<E: ev::EventDescriptor + 'static>(
|
|
self,
|
|
event: E,
|
|
#[allow(unused_mut)] mut event_handler: impl FnMut(E::EventType) + 'static,
|
|
) -> Self {
|
|
cfg_if::cfg_if! {
|
|
if #[cfg(debug_assertions)] {
|
|
trace!("calling on() {}", event.name());
|
|
let span = ::tracing::Span::current();
|
|
let event_handler = move |e| {
|
|
let _guard = span.enter();
|
|
event_handler(e);
|
|
};
|
|
}
|
|
}
|
|
|
|
self.on_impl(event, Box::new(event_handler))
|
|
}
|
|
|
|
fn on_impl<E: ev::EventDescriptor + 'static>(
|
|
self,
|
|
event: E,
|
|
event_handler: Box<dyn FnMut(E::EventType)>,
|
|
) -> Self {
|
|
cfg_if! {
|
|
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
|
match &self {
|
|
Self::Element(el) => {
|
|
if E::BUBBLES {
|
|
add_event_listener(&el.element, event.event_delegation_key(), event.name(), event_handler, &None);
|
|
} else {
|
|
add_event_listener_undelegated(
|
|
&el.element,
|
|
&event.name(),
|
|
event_handler,
|
|
&None,
|
|
);
|
|
}
|
|
}
|
|
Self::Component(c) => {
|
|
let event_handler = Rc::new(RefCell::new(event_handler));
|
|
|
|
c.children.iter().cloned().for_each(|c| {
|
|
let event_handler = Rc::clone(&event_handler);
|
|
|
|
_ = c.on(event.clone(), Box::new(move |e| event_handler.borrow_mut()(e)));
|
|
});
|
|
}
|
|
Self::CoreComponent(c) => match c {
|
|
CoreComponent::DynChild(d) => {
|
|
if let Some(subview) = *d.child.take() {
|
|
let subview = subview.on(event, event_handler);
|
|
d.child.replace(Box::new(Some(subview)));
|
|
}
|
|
}
|
|
CoreComponent::Each(each) => {
|
|
let event_handler = Rc::new(RefCell::new(event_handler));
|
|
let new_children = each.children.take().into_iter().map(|item| {
|
|
if let Some(mut item) = item {
|
|
let event_handler = Rc::clone(&event_handler);
|
|
item.child = item.child.on(event.clone(), Box::new(move |e| event_handler.borrow_mut()(e)));
|
|
Some(item)
|
|
} else {
|
|
None
|
|
}
|
|
}).collect::<Vec<_>>();
|
|
each.children.replace(new_children);
|
|
}
|
|
CoreComponent::Unit(_) => {}
|
|
},
|
|
_ => {}
|
|
}
|
|
} else {
|
|
_ = event;
|
|
_ = event_handler;
|
|
}
|
|
}
|
|
|
|
self
|
|
}
|
|
|
|
/// Adds a directive analogous to [`HtmlElement::directive`].
|
|
///
|
|
/// This method will attach directive to **all** child
|
|
/// [`HtmlElement`] children.
|
|
#[inline(always)]
|
|
pub fn directive<T, P>(
|
|
self,
|
|
handler: impl Directive<T, P> + 'static,
|
|
param: P,
|
|
) -> Self
|
|
where
|
|
T: ?Sized + 'static,
|
|
P: Clone + 'static,
|
|
{
|
|
cfg_if::cfg_if! {
|
|
if #[cfg(debug_assertions)] {
|
|
trace!("calling directive()");
|
|
let span = ::tracing::Span::current();
|
|
let handler = move |e, p| {
|
|
let _guard = span.enter();
|
|
handler.run(e, p);
|
|
};
|
|
}
|
|
}
|
|
|
|
self.directive_impl(Box::new(handler), param)
|
|
}
|
|
|
|
fn directive_impl<T, P>(
|
|
self,
|
|
handler: Box<dyn Directive<T, P>>,
|
|
param: P,
|
|
) -> Self
|
|
where
|
|
T: ?Sized + 'static,
|
|
P: Clone + 'static,
|
|
{
|
|
cfg_if! { if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
|
match &self {
|
|
Self::Element(el) => {
|
|
let _ = el.clone().into_html_element().directive(handler, param);
|
|
}
|
|
Self::Component(c) => {
|
|
let handler = Rc::from(handler);
|
|
|
|
for child in c.children.iter().cloned() {
|
|
let _ = child.directive(Rc::clone(&handler), param.clone());
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
} else {
|
|
let _ = handler;
|
|
let _ = param;
|
|
}}
|
|
|
|
self
|
|
}
|
|
}
|
|
|
|
#[cfg_attr(debug_assertions, instrument)]
|
|
#[track_caller]
|
|
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
|
#[doc(hidden)]
|
|
pub fn mount_child<GWSN: Mountable + fmt::Debug>(
|
|
kind: MountKind,
|
|
child: &GWSN,
|
|
) {
|
|
let child = child.get_mountable_node();
|
|
|
|
match kind {
|
|
MountKind::Append(el) => {
|
|
el.append_child(&child)
|
|
.expect("append operation to not err");
|
|
}
|
|
MountKind::Before(closing) => {
|
|
closing
|
|
.unchecked_ref::<web_sys::Element>()
|
|
.before_with_node_1(&child)
|
|
.expect("before to not err");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
|
#[track_caller]
|
|
fn unmount_child(start: &web_sys::Node, end: &web_sys::Node) {
|
|
let mut sibling = start.clone();
|
|
|
|
while sibling != *end {
|
|
if let Some(next_sibling) = sibling.next_sibling() {
|
|
sibling.unchecked_ref::<web_sys::Element>().remove();
|
|
|
|
sibling = next_sibling;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Similar to [`unmount_child`], but instead of removing entirely
|
|
/// from the DOM, it inserts all child nodes into the [`DocumentFragment`].
|
|
///
|
|
/// [DocumentFragment]: web_sys::DocumentFragment
|
|
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
|
#[track_caller]
|
|
fn prepare_to_move(
|
|
frag: &web_sys::DocumentFragment,
|
|
opening: &web_sys::Node,
|
|
closing: &web_sys::Node,
|
|
) {
|
|
let mut sibling = opening.clone();
|
|
|
|
while sibling != *closing {
|
|
if let Some(next_sibling) = sibling.next_sibling() {
|
|
frag.append_child(&sibling).unwrap();
|
|
|
|
sibling = next_sibling;
|
|
} else {
|
|
frag.append_child(&sibling).unwrap();
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
frag.append_child(closing).unwrap();
|
|
}
|
|
|
|
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
|
#[derive(Debug)]
|
|
#[doc(hidden)]
|
|
pub enum MountKind<'a> {
|
|
Before(
|
|
// The closing node
|
|
&'a web_sys::Node,
|
|
),
|
|
Append(&'a web_sys::Node),
|
|
}
|
|
|
|
/// Runs the provided closure and mounts the result to the `<body>`.
|
|
pub fn mount_to_body<F, N>(f: F)
|
|
where
|
|
F: FnOnce() -> N + 'static,
|
|
N: IntoView,
|
|
{
|
|
#[cfg(all(feature = "web", feature = "ssr"))]
|
|
crate::logging::console_warn(
|
|
"You have both `csr` and `ssr` or `hydrate` and `ssr` enabled as \
|
|
features, which may cause issues like <Suspense/>` failing to work \
|
|
silently.",
|
|
);
|
|
|
|
cfg_if! {
|
|
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
|
mount_to(crate::document().body().expect("body element to exist"), f)
|
|
} else {
|
|
_ = f;
|
|
crate::warn!("`mount_to_body` should not be called outside the browser.");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Runs the provided closure and mounts the result to the provided element.
|
|
pub fn mount_to<F, N>(parent: web_sys::HtmlElement, f: F)
|
|
where
|
|
F: FnOnce() -> N + 'static,
|
|
N: IntoView,
|
|
{
|
|
mount_to_with_stop_hydrating(parent, true, f)
|
|
}
|
|
|
|
/// Runs the provided closure and mounts the result to the provided element.
|
|
pub fn mount_to_with_stop_hydrating<F, N>(
|
|
parent: web_sys::HtmlElement,
|
|
stop_hydrating: bool,
|
|
f: F,
|
|
) where
|
|
F: FnOnce() -> N + 'static,
|
|
N: IntoView,
|
|
{
|
|
cfg_if! {
|
|
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
|
let node = f().into_view();
|
|
if stop_hydrating {
|
|
HydrationCtx::stop_hydrating();
|
|
}
|
|
if cfg!(feature = "csr") {
|
|
parent.append_child(&node.get_mountable_node()).unwrap();
|
|
}
|
|
std::mem::forget(node);
|
|
} else {
|
|
_ = parent;
|
|
_ = f;
|
|
_ = stop_hydrating;
|
|
crate::warn!("`mount_to` should not be called outside the browser.");
|
|
}
|
|
}
|
|
}
|
|
|
|
thread_local! {
|
|
pub(crate) static WINDOW: web_sys::Window = web_sys::window().unwrap_throw();
|
|
|
|
pub(crate) static DOCUMENT: web_sys::Document = web_sys::window().unwrap_throw().document().unwrap_throw();
|
|
}
|
|
|
|
/// Returns the [`Window`](https://developer.mozilla.org/en-US/docs/Web/API/Window).
|
|
///
|
|
/// This is cached as a thread-local variable, so calling `window()` multiple times
|
|
/// requires only one call out to JavaScript.
|
|
pub fn window() -> web_sys::Window {
|
|
WINDOW.with(Clone::clone)
|
|
}
|
|
|
|
/// Returns the [`Document`](https://developer.mozilla.org/en-US/docs/Web/API/Document).
|
|
///
|
|
/// This is cached as a thread-local variable, so calling `document()` multiple times
|
|
/// requires only one call out to JavaScript.
|
|
pub fn document() -> web_sys::Document {
|
|
DOCUMENT.with(Clone::clone)
|
|
}
|
|
|
|
/// Returns true if running on the server (SSR).
|
|
///
|
|
/// In the past, this was implemented by checking whether `not(target_arch = "wasm32")`.
|
|
/// Now that some cloud platforms are moving to run Wasm on the edge, we really can't
|
|
/// guarantee that compiling to Wasm means browser APIs are available, or that not compiling
|
|
/// to Wasm means we're running on the server.
|
|
///
|
|
/// ```
|
|
/// # use leptos_dom::is_server;
|
|
/// let todos = if is_server() {
|
|
/// // if on the server, load from DB
|
|
/// } else {
|
|
/// // if on the browser, do something else
|
|
/// };
|
|
/// ```
|
|
pub const fn is_server() -> bool {
|
|
!is_browser()
|
|
}
|
|
|
|
/// Returns true if running on the browser (CSR).
|
|
///
|
|
/// ```
|
|
/// # use leptos_dom::is_browser;
|
|
/// let todos = if is_browser() {
|
|
/// // if on the browser, call `wasm_bindgen` methods
|
|
/// } else {
|
|
/// // if on the server, do something else
|
|
/// };
|
|
/// ```
|
|
pub const fn is_browser() -> bool {
|
|
cfg!(all(target_arch = "wasm32", feature = "web"))
|
|
}
|
|
|
|
/// Returns true if `debug_assertions` are enabled.
|
|
/// ```
|
|
/// # use leptos_dom::is_dev;
|
|
/// if is_dev() {
|
|
/// // log something or whatever
|
|
/// }
|
|
/// ```
|
|
pub const fn is_dev() -> bool {
|
|
cfg!(debug_assertions)
|
|
}
|
|
|
|
/// Returns true if `debug_assertions` are disabled.
|
|
pub const fn is_release() -> bool {
|
|
!is_dev()
|
|
}
|
|
|
|
macro_rules! impl_into_view_for_tuples {
|
|
($($ty:ident),* $(,)?) => {
|
|
impl<$($ty),*> IntoView for ($($ty,)*)
|
|
where
|
|
$($ty: IntoView),*
|
|
{
|
|
#[inline]
|
|
fn into_view(self) -> View {
|
|
paste::paste! {
|
|
let ($([<$ty:lower>],)*) = self;
|
|
[
|
|
$([<$ty:lower>].into_view()),*
|
|
].into_view()
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
impl_into_view_for_tuples!(A);
|
|
impl_into_view_for_tuples!(A, B);
|
|
impl_into_view_for_tuples!(A, B, C);
|
|
impl_into_view_for_tuples!(A, B, C, D);
|
|
impl_into_view_for_tuples!(A, B, C, D, E);
|
|
impl_into_view_for_tuples!(A, B, C, D, E, F);
|
|
impl_into_view_for_tuples!(A, B, C, D, E, F, G);
|
|
impl_into_view_for_tuples!(A, B, C, D, E, F, G, H);
|
|
impl_into_view_for_tuples!(A, B, C, D, E, F, G, H, I);
|
|
impl_into_view_for_tuples!(A, B, C, D, E, F, G, H, I, J);
|
|
impl_into_view_for_tuples!(A, B, C, D, E, F, G, H, I, J, K);
|
|
impl_into_view_for_tuples!(A, B, C, D, E, F, G, H, I, J, K, L);
|
|
impl_into_view_for_tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M);
|
|
impl_into_view_for_tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N);
|
|
impl_into_view_for_tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
|
|
impl_into_view_for_tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
|
|
impl_into_view_for_tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q);
|
|
impl_into_view_for_tuples!(
|
|
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R
|
|
);
|
|
impl_into_view_for_tuples!(
|
|
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S
|
|
);
|
|
impl_into_view_for_tuples!(
|
|
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T
|
|
);
|
|
impl_into_view_for_tuples!(
|
|
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U
|
|
);
|
|
impl_into_view_for_tuples!(
|
|
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V
|
|
);
|
|
impl_into_view_for_tuples!(
|
|
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W
|
|
);
|
|
impl_into_view_for_tuples!(
|
|
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X
|
|
);
|
|
impl_into_view_for_tuples!(
|
|
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
|
|
);
|
|
impl_into_view_for_tuples!(
|
|
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! api_planning {
|
|
($($tt:tt)*) => {};
|
|
}
|
|
|
|
api_planning! {
|
|
let c = Component::<Props, ChildKind>::new("MyComponent")
|
|
.props(Props::default()) // Can only be called once
|
|
.child(Child1) // Anything that impl Into<ChildKind>
|
|
.child(Child2);
|
|
}
|
|
|
|
impl IntoView for String {
|
|
#[cfg_attr(
|
|
any(debug_assertions, feature = "ssr"),
|
|
instrument(level = "trace", name = "#text", skip_all)
|
|
)]
|
|
#[inline(always)]
|
|
fn into_view(self) -> View {
|
|
View::Text(Text::new(self.into()))
|
|
}
|
|
}
|
|
|
|
impl IntoView for &'static str {
|
|
#[cfg_attr(
|
|
any(debug_assertions, feature = "ssr"),
|
|
instrument(level = "trace", name = "#text", skip_all)
|
|
)]
|
|
#[inline(always)]
|
|
fn into_view(self) -> View {
|
|
View::Text(Text::new(self.into()))
|
|
}
|
|
}
|
|
|
|
impl IntoView for Cow<'static, str> {
|
|
#[cfg_attr(
|
|
any(debug_assertions, feature = "ssr"),
|
|
instrument(level = "info", name = "#text", skip_all)
|
|
)]
|
|
#[inline(always)]
|
|
fn into_view(self) -> View {
|
|
View::Text(Text::new(self.into()))
|
|
}
|
|
}
|
|
|
|
impl IntoView for Rc<str> {
|
|
#[cfg_attr(
|
|
any(debug_assertions, feature = "ssr"),
|
|
instrument(level = "trace", name = "#text", skip_all)
|
|
)]
|
|
#[inline(always)]
|
|
fn into_view(self) -> View {
|
|
View::Text(Text::new(self.into()))
|
|
}
|
|
}
|
|
|
|
impl IntoView for Oco<'static, str> {
|
|
#[cfg_attr(
|
|
any(debug_assertions, feature = "ssr"),
|
|
instrument(level = "trace", name = "#text", skip_all)
|
|
)]
|
|
#[inline(always)]
|
|
fn into_view(self) -> View {
|
|
View::Text(Text::new(self))
|
|
}
|
|
}
|
|
|
|
impl<V> IntoView for Vec<V>
|
|
where
|
|
V: IntoView,
|
|
{
|
|
#[cfg_attr(
|
|
any(debug_assertions, feature = "ssr"),
|
|
instrument(level = "trace", name = "#text", skip_all)
|
|
)]
|
|
fn into_view(self) -> View {
|
|
self.into_iter()
|
|
.map(|v| v.into_view())
|
|
.collect::<Fragment>()
|
|
.into_view()
|
|
}
|
|
}
|
|
|
|
impl IntoView for core::fmt::Arguments<'_> {
|
|
#[cfg_attr(
|
|
any(debug_assertions, feature = "ssr"),
|
|
instrument(level = "trace", name = "#text", skip_all)
|
|
)]
|
|
fn into_view(self) -> View {
|
|
match self.as_str() {
|
|
Some(s) => s.into_view(),
|
|
None => self.to_string().into_view(),
|
|
}
|
|
}
|
|
}
|
|
|
|
macro_rules! viewable_primitive {
|
|
($($child_type:ty),* $(,)?) => {
|
|
$(
|
|
impl IntoView for $child_type {
|
|
#[inline(always)]
|
|
fn into_view(self) -> View {
|
|
View::Text(Text::new(self.to_string().into()))
|
|
}
|
|
}
|
|
)*
|
|
};
|
|
}
|
|
|
|
viewable_primitive![
|
|
&String,
|
|
usize,
|
|
u8,
|
|
u16,
|
|
u32,
|
|
u64,
|
|
u128,
|
|
isize,
|
|
i8,
|
|
i16,
|
|
i32,
|
|
i64,
|
|
i128,
|
|
f32,
|
|
f64,
|
|
char,
|
|
bool,
|
|
std::net::IpAddr,
|
|
std::net::SocketAddr,
|
|
std::net::SocketAddrV4,
|
|
std::net::SocketAddrV6,
|
|
std::net::Ipv4Addr,
|
|
std::net::Ipv6Addr,
|
|
std::char::ToUppercase,
|
|
std::char::ToLowercase,
|
|
std::num::NonZeroI8,
|
|
std::num::NonZeroU8,
|
|
std::num::NonZeroI16,
|
|
std::num::NonZeroU16,
|
|
std::num::NonZeroI32,
|
|
std::num::NonZeroU32,
|
|
std::num::NonZeroI64,
|
|
std::num::NonZeroU64,
|
|
std::num::NonZeroI128,
|
|
std::num::NonZeroU128,
|
|
std::num::NonZeroIsize,
|
|
std::num::NonZeroUsize,
|
|
std::panic::Location<'_>,
|
|
];
|
|
|
|
cfg_if! {
|
|
if #[cfg(feature = "nightly")] {
|
|
viewable_primitive! {
|
|
std::backtrace::Backtrace
|
|
}
|
|
}
|
|
}
|