diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index d5c2f60e2..c4fc55f3b 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -20,6 +20,7 @@ default = ["csr", "serde"] csr = [ "leptos_core/csr", "leptos_dom/csr", + "leptos_dom/web", "leptos_macro/csr", "leptos_reactive/csr", "leptos_server/csr", @@ -27,6 +28,7 @@ csr = [ hydrate = [ "leptos_core/hydrate", "leptos_dom/hydrate", + "leptos_dom/web", "leptos_macro/hydrate", "leptos_reactive/hydrate", "leptos_server/hydrate", diff --git a/leptos_core/Cargo.toml b/leptos_core/Cargo.toml index fc50fb53f..c35945ff7 100644 --- a/leptos_core/Cargo.toml +++ b/leptos_core/Cargo.toml @@ -12,6 +12,7 @@ leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0. leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.18" } leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.18" } log = "0.4" +typed-builder = "0.11" [dev-dependencies] leptos = { path = "../leptos", default-features = false, version = "0.0" } diff --git a/leptos_core/src/lib.rs b/leptos_core/src/lib.rs index a3bef651f..1c1608d30 100644 --- a/leptos_core/src/lib.rs +++ b/leptos_core/src/lib.rs @@ -3,20 +3,12 @@ //! This crate contains several utility pieces that depend on multiple crates. //! They are all re-exported in the main `leptos` crate. -mod for_component; -mod map; -mod suspense; +//mod for_component; +//mod map; +//mod suspense; -pub use for_component::*; -pub use map::*; -pub use suspense::*; +//pub use for_component::*; +//pub use map::*; +//pub use suspense::*; -/// Describes the properties of a component. This is typically generated by the `Prop` derive macro -/// as part of the `#[component]` macro. -pub trait Prop { - /// Builder type, automatically generated. - type Builder; - - /// The builder should be automatically generated using the `Prop` derive macro. - fn builder() -> Self::Builder; -} +pub use typed_builder::TypedBuilder; diff --git a/leptos_dom/src/components.rs b/leptos_dom/src/components.rs index d96032c5d..e71904740 100644 --- a/leptos_dom/src/components.rs +++ b/leptos_dom/src/components.rs @@ -44,8 +44,11 @@ pub struct ComponentRepr { impl Drop for ComponentRepr { fn drop(&mut self) { + // TODO: all ComponentReprs are immediately dropped, + // which means their scopes are immediately disposed, + // which means components have no reactivity if let Some(disposer) = self.disposer.take() { - disposer.dispose(); + //disposer.dispose(); } } } @@ -130,7 +133,7 @@ impl ComponentRepr { /// A user-defined `leptos` component. pub struct Component where - F: FnOnce(Scope) -> Vec, + F: FnOnce(Scope) -> Node, { name: Cow<'static, str>, children_fn: F, @@ -138,7 +141,7 @@ where impl Component where - F: FnOnce(Scope) -> Vec, + F: FnOnce(Scope) -> Node, { /// Creates a new component. pub fn new(name: impl Into>, f: F) -> Self { @@ -151,21 +154,21 @@ where impl IntoNode for Component where - F: FnOnce(Scope) -> Vec, + F: FnOnce(Scope) -> Node, { fn into_node(self, cx: Scope) -> Node { let Self { name, children_fn } = self; let mut children = None; - let disposer = cx.child_scope(|cx| children = Some(children_fn(cx))); + let disposer = cx.child_scope(|cx| children = Some(cx.untrack(move || children_fn(cx)))); let children = children.unwrap(); let mut repr = ComponentRepr::new(name); repr.disposer = Some(disposer); - repr.children = children; + repr.children = vec![children]; repr.into_node(cx) } diff --git a/leptos_dom/src/components/each.rs b/leptos_dom/src/components/each.rs index d446ec71b..3c97b4f17 100644 --- a/leptos_dom/src/components/each.rs +++ b/leptos_dom/src/components/each.rs @@ -5,6 +5,7 @@ use itertools::{EitherOrBoth, Itertools}; use leptos_reactive::{create_effect, Scope}; use rustc_hash::FxHasher; use smallvec::{smallvec, SmallVec}; +use typed_builder::TypedBuilder; use std::{ borrow::Cow, cell::RefCell, @@ -388,7 +389,7 @@ fn diff(from: &FxIndexSet, to: &FxIndexSet) -> Diff { } } - normalized_idx += 1; + normalized_idx = normalized_idx.wrapping_add(1); } let mut diffs = Diff { @@ -598,3 +599,78 @@ fn apply_cmds( // items children.drain_filter(|c| c.is_none()); } + + +/// Properties for the [For](crate::For) component, a keyed list. +#[derive(TypedBuilder)] +pub struct ForProps +where + IF: Fn() -> I + 'static, + I: IntoIterator, + EF: Fn(T) -> N + 'static, + N: IntoNode, + KF: Fn(&T) -> K + 'static, + K: Eq + Hash + 'static, + T: 'static, +{ + /// Items over which the component should iterate. + pub each: IF, + /// A key function that will be applied to each item + pub key: KF, + /// Should provide a single child function, which takes + pub children: Box Vec>, +} + +/// Iterates over children and displays them, keyed by the `key` function given. +/// +/// This is much more efficient than naively iterating over nodes with `.iter().map(|n| view! { cx, ... })...`, +/// as it avoids re-creating DOM nodes that are not being changed. +/// +/// ``` +/// # use leptos::*; +/// +/// #[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// struct Counter { +/// id: usize, +/// count: RwSignal +/// } +/// +/// fn Counters(cx: Scope) -> Element { +/// let (counters, set_counters) = create_signal::>(cx, vec![]); +/// +/// view! { +/// cx, +///
+/// +/// {|cx: Scope, counter: &Counter| { +/// let count = counter.count; +/// view! { +/// cx, +/// +/// } +/// } +/// } +/// +///
+/// } +/// } +/// ``` +#[allow(non_snake_case)] +pub fn For(cx: Scope, props: ForProps) -> Node +where + IF: Fn() -> I + 'static, + I: IntoIterator, + EF: Fn(T) -> N + 'static, + N: IntoNode, + KF: Fn(&T) -> K + 'static, + K: Eq + Hash + 'static, + T: 'static, +{ + let each_fn = (props.children)().swap_remove(0); + EachKey::new(props.each, props.key, each_fn).into_node(cx) +} \ No newline at end of file diff --git a/leptos_dom/src/events/typed.rs b/leptos_dom/src/events/typed.rs index 180330c2f..1e3460edc 100644 --- a/leptos_dom/src/events/typed.rs +++ b/leptos_dom/src/events/typed.rs @@ -76,7 +76,7 @@ macro_rules! generate_event_types { pub struct $event; impl EventDescriptor for $event { - type EventType = web_sys::MouseEvent; + type EventType = web_sys::$web_sys_event; fn name(&self) -> Cow<'static, str> { concat!("on", stringify!([<$event:lower>])).into() diff --git a/leptos_dom/src/html.rs b/leptos_dom/src/html.rs index e0b9a76af..fe1249791 100644 --- a/leptos_dom/src/html.rs +++ b/leptos_dom/src/html.rs @@ -409,7 +409,7 @@ impl HtmlElement { } } else { - _ = event_name; + _ = event; _ = event_handler; } } diff --git a/leptos_dom/src/macro_helpers/into_child.rs b/leptos_dom/src/macro_helpers/into_child.rs index 4a910b105..1bbb4444f 100644 --- a/leptos_dom/src/macro_helpers/into_child.rs +++ b/leptos_dom/src/macro_helpers/into_child.rs @@ -1,7 +1,7 @@ use std::{cell::{OnceCell, RefCell}, hash::Hash, rc::Rc}; use cfg_if::cfg_if; use leptos_reactive::{Scope, create_effect}; -use crate::{IntoNode, ComponentRepr, EachKey, Node, HtmlElement, Text, Element, Fragment, Unit, text, DynChild, IntoElement}; +use crate::{IntoNode, ComponentRepr, EachKey, Node, HtmlElement, Text, Element, Fragment, Unit, text, DynChild, IntoElement, Component}; pub enum Child { /// A (presumably reactive) function, which will be run inside an effect to do targeted updates to the node. @@ -43,7 +43,7 @@ impl IntoChild for Node { impl IntoChild for String { fn into_child(self, _cx: Scope) -> Child { - Child::Text(self.into()) + Child::Text(self) } } @@ -102,12 +102,22 @@ node_type!(Vec); node_type!(Fragment); node_type!(ComponentRepr); + impl IntoChild for HtmlElement { fn into_child(self, cx: Scope) -> Child { Child::Node(self.into_node(cx)) } } +impl IntoChild for Component +where + F: FnOnce(Scope) -> Node, +{ + fn into_child(self, cx: Scope) -> Child { + Child::Node(self.into_node(cx)) + } +} + macro_rules! text_type { ($child_type:ty) => { impl IntoChild for $child_type { diff --git a/leptos_macro/src/component.rs b/leptos_macro/src/component.rs index 339d15900..8b57f98e2 100644 --- a/leptos_macro/src/component.rs +++ b/leptos_macro/src/component.rs @@ -121,11 +121,7 @@ impl ToTokens for InlinePropsBody { None }; - //let modifiers = if first_lifetime.is_some() { - let modifiers = quote! { #[derive(Props)] }; - /* } else { - quote! { #[derive(Props, PartialEq, Eq)] } - }; */ + let modifiers = quote! { #[derive(leptos::TypedBuilder)] }; let (_scope_lifetime, fn_generics, struct_generics) = if let Some(lt) = first_lifetime { let struct_generics: Punctuated<_, token::Comma> = generics diff --git a/leptos_macro/src/view.rs b/leptos_macro/src/view.rs index b9812d493..56029d971 100644 --- a/leptos_macro/src/view.rs +++ b/leptos_macro/src/view.rs @@ -136,7 +136,7 @@ fn node_to_tokens(cx: &Ident, node: &Node, mode: Mode) -> TokenStream { let span = node.value.span(); let value = node.value.as_ref(); quote_spanned! { - span => leptos::text(#value) + span => text(#value) } }, Node::Block(node) => { @@ -154,7 +154,7 @@ fn node_to_tokens(cx: &Ident, node: &Node, mode: Mode) -> TokenStream { fn element_to_tokens(cx: &Ident, node: &NodeElement, mode: Mode) -> TokenStream { let span = node.name.span(); if is_component_node(node) { - todo!("component node") + component_to_tokens(cx, node, mode) } else { let name = if is_custom_element(&node.name) { let name = node.name.to_string(); @@ -171,7 +171,27 @@ fn element_to_tokens(cx: &Ident, node: &NodeElement, mode: Mode) -> TokenStream } }); let children = node.children.iter().map(|node| { - let child = node_to_tokens(cx, node, mode); + let child = match node { + Node::Fragment(fragment) => { + fragment_to_tokens(cx, Span::call_site(), &fragment.children, mode) + }, + Node::Text(node) => { + let span = node.value.span(); + let value = node.value.as_ref(); + quote_spanned! { + span => #[allow(unused_braces)] #value + } + }, + Node::Block(node) => { + let span = node.value.span(); + let value = node.value.as_ref(); + quote_spanned! { + span => #[allow(unused_braces)] #value + } + }, + Node::Element(node) => element_to_tokens(cx, node, mode), + Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => quote! { }, + }; quote! { ._child(cx, #child) } @@ -192,7 +212,7 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute, mode: Mode) -> TokenStr if mode != Mode::Ssr { let value = node.value.as_ref().and_then(|expr| expr_to_ident(expr)).expect("'_ref' needs to be passed a variable name"); quote_spanned! { - span => ._ref(#value) + span => ._ref(#[allow(unused_braces)] #value) } } else { todo!() @@ -209,11 +229,11 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute, mode: Mode) -> TokenStr if NON_BUBBLING_EVENTS.contains(&name) { quote_spanned! { - span => .on::(#name, #handler) + span => .on::(#name, #[allow(unused_braces)] #handler) } } else { quote_spanned! { - span => .on_delegated::(#name, #handler) + span => .on_delegated::(#name, #[allow(unused_braces)] #handler) } } } else { @@ -223,7 +243,7 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute, mode: Mode) -> TokenStr let value = node.value.as_ref().expect("prop: attributes need a value").as_ref(); if mode != Mode::Ssr { quote_spanned! { - span => ._prop(#cx, #name, #value) + span => ._prop(#cx, #name, #[allow(unused_braces)] #value) } } else { todo!() @@ -232,14 +252,13 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute, mode: Mode) -> TokenStr let value = node.value.as_ref().expect("class: attributes need a value").as_ref(); if mode != Mode::Ssr { quote_spanned! { - span => ._class(#cx, #name, #value) + span => ._class(#cx, #name, #[allow(unused_braces)] #value) } } else { todo!() } } else { let name = name.replacen("attr:", "", 1); - let node_name = node.key; let value = match node.value.as_ref() { Some(value) => { let value = value.as_ref(); @@ -250,7 +269,7 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute, mode: Mode) -> TokenStr }; if mode != Mode::Ssr { quote_spanned! { - span => ._attr(#cx, #name, #value) + span => ._attr(#cx, #name, #[allow(unused_braces)] #value) } } else { quote! { } @@ -258,6 +277,91 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute, mode: Mode) -> TokenStr } } +fn component_to_tokens(cx: &Ident, node: &NodeElement, mode: Mode) -> TokenStream { + let name = &node.name; + let component_name = ident_from_tag_name(&node.name); + let component_name_str = name.to_string(); + let span = node.name.span(); + let component_props_name = Ident::new(&format!("{component_name}Props"), span); + + let children = if node.children.is_empty() { + quote! {} + } else if node.children.len() == 1 { + let child = component_child(cx, &node.children[0], mode); + quote_spanned! { span => .children(Box::new(move || vec![#child])) } + } else { + let children = node.children.iter() + .map(|node| component_child(cx, node, mode)); + quote_spanned! { span => .children(Box::new(move || vec![#(#children),*])) } + }; + + let props = node.attributes.iter() + .filter_map(|node| if let Node::Attribute(node) = node { Some(node) } else { None }) + .map(|attr| { + let name = &attr.key; + let span = attr.key.span(); + let value = attr + .value + .as_ref() + .map(|v| { + let v = v.as_ref(); + quote_spanned! { span => #v } + }) + .unwrap_or_else(|| quote_spanned! { span => #name }); + + quote_spanned! { + span => .#name(#[allow(unused_braces)] #value) + } + }); + + let component_itself = quote_spanned! { + span => #name( + cx, + #component_props_name::builder() + #(#props)* + #children + .build(), + ) + }; + + quote_spanned! { + span => leptos::Component::new( + #component_name_str, + move |cx| #component_itself + ) + } +} + +fn component_child(cx: &Ident, node: &Node, mode: Mode) -> TokenStream { + match node { + Node::Block(node) => { + let span = node.value.span(); + let value = node.value.as_ref(); + quote_spanned! { + span => #value + } + }, + _ => node_to_tokens(cx, node, mode) + } +} + +fn ident_from_tag_name(tag_name: &NodeName) -> Ident { + match tag_name { + NodeName::Path(path) => path + .path + .segments + .iter() + .last() + .map(|segment| segment.ident.clone()) + .expect("element needs to have a name"), + NodeName::Block(_) => panic!("blocks not allowed in tag-name position"), + _ => Ident::new( + &tag_name.to_string().replace(['-', ':'], "_"), + tag_name.span(), + ), + } +} + fn expr_to_ident(expr: &syn::Expr) -> Option<&ExprPath> { match expr { syn::Expr::Block(block) => block.block.stmts.last().and_then(|stmt| {