feature: reintroduce limited template-node cloning w/ `template` macro (#526)

This commit is contained in:
Greg Johnston 2023-02-16 07:02:01 -05:00 committed by GitHub
parent 8d42e91eb8
commit 0071a48b8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 607 additions and 14 deletions

View File

@ -11,9 +11,28 @@ thread_local! {
pub(crate) static GLOBAL_EVENTS: RefCell<HashSet<Cow<'static, str>>> = RefCell::new(HashSet::new());
}
/// Adds an event listener to the target DOM element using implicit event delegation.
// Used in template macro
#[doc(hidden)]
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn add_event_listener<E>(
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();
if event.bubbles() {
add_event_listener(target, event_name, event_handler);
} else {
add_event_listener_undelegated(target, &event_name, event_handler);
}
}
/// 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,
event_name: Cow<'static, str>,
#[cfg(debug_assertions)] mut cb: impl FnMut(E) + 'static,

View File

@ -23,6 +23,8 @@ pub mod svg;
mod transparent;
use cfg_if::cfg_if;
pub use components::*;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub use events::add_event_helper;
pub use events::typed as ev;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use events::{add_event_listener, add_event_listener_undelegated};
@ -60,7 +62,8 @@ pub trait IntoView {
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
trait Mountable {
#[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.
@ -138,9 +141,11 @@ cfg_if! {
/// HTML element.
#[derive(Clone, PartialEq, Eq)]
pub struct Element {
#[doc(hidden)]
#[cfg(debug_assertions)]
name: Cow<'static, str>,
element: web_sys::HtmlElement,
pub name: Cow<'static, str>,
#[doc(hidden)]
pub element: web_sys::HtmlElement,
}
impl fmt::Debug for Element {
@ -615,7 +620,11 @@ impl View {
#[cfg_attr(debug_assertions, instrument)]
#[track_caller]
#[cfg(all(target_arch = "wasm32", feature = "web"))]
fn mount_child<GWSN: Mountable + fmt::Debug>(kind: MountKind, child: &GWSN) {
#[doc(hidden)]
pub fn mount_child<GWSN: Mountable + fmt::Debug>(
kind: MountKind,
child: &GWSN,
) {
let child = child.get_mountable_node();
match kind {
@ -678,7 +687,8 @@ fn prepare_to_move(
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[derive(Debug)]
enum MountKind<'a> {
#[doc(hidden)]
pub enum MountKind<'a> {
Before(
// The closing node
&'a web_sys::Node,

View File

@ -254,7 +254,8 @@ attr_type!(char);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use std::borrow::Cow;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn attribute_helper(
#[doc(hidden)]
pub fn attribute_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
value: Attribute,

View File

@ -71,7 +71,8 @@ impl<T: IntoClass> IntoClass for (Scope, T) {
use std::borrow::Cow;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn class_helper(
#[doc(hidden)]
pub fn class_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
value: Class,

View File

@ -25,6 +25,7 @@ leptos_dom = { workspace = true }
leptos_reactive = { workspace = true }
leptos_server = { workspace = true }
convert_case = "0.6.0"
uuid = { version = "1", features = ["v4"] }
[dev-dependencies]
log = "0.4"

View File

@ -32,9 +32,11 @@ impl Default for Mode {
mod params;
mod view;
use template::render_template;
use view::render_view;
mod component;
mod server;
mod template;
/// The `view` macro uses RSX (like JSX, but Rust!) It follows most of the
/// same rules as HTML, with the following differences:
@ -344,6 +346,43 @@ pub fn view(tokens: TokenStream) -> TokenStream {
}
}
/// An optimized, cached template for client-side rendering. Follows the same
/// syntax as the [view](crate::macro) macro. In hydration or server-side rendering mode,
/// behaves exactly as the `view` macro. In client-side rendering mode, uses a `<template>`
/// node to efficiently render the element. Should only be used with a single root element.
#[proc_macro_error::proc_macro_error]
#[proc_macro]
pub fn template(tokens: TokenStream) -> TokenStream {
if cfg!(feature = "csr") {
let tokens: proc_macro2::TokenStream = tokens.into();
let mut tokens = tokens.into_iter();
let (cx, comma) = (tokens.next(), tokens.next());
match (cx, comma) {
(Some(TokenTree::Ident(cx)), Some(TokenTree::Punct(punct)))
if punct.as_char() == ',' =>
{
match parse(tokens.collect::<proc_macro2::TokenStream>().into())
{
Ok(nodes) => render_template(
&proc_macro2::Ident::new(&cx.to_string(), cx.span()),
&nodes,
),
Err(error) => error.to_compile_error(),
}
.into()
}
_ => {
panic!(
"view! macro needs a context and RSX: e.g., view! {{ cx, \
<div>...</div> }}"
)
}
}
} else {
view(tokens)
}
}
/// Annotates a function so that it can be used with your template as a Leptos `<Component/>`.
///
/// The `#[component]` macro allows you to annotate plain Rust functions as components

View File

@ -0,0 +1,522 @@
use crate::is_component_node;
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeValueExpr};
use uuid::Uuid;
pub(crate) fn render_template(cx: &Ident, nodes: &[Node]) -> TokenStream {
let template_uid = Ident::new(
&format!("TEMPLATE_{}", Uuid::new_v4().simple()),
Span::call_site(),
);
if nodes.len() == 1 {
first_node_to_tokens(cx, &template_uid, &nodes[0])
} else {
panic!("template! takes a single root element.")
}
}
fn first_node_to_tokens(
cx: &Ident,
template_uid: &Ident,
node: &Node,
) -> TokenStream {
match node {
Node::Element(node) => root_element_to_tokens(cx, template_uid, node),
_ => panic!("template! takes a single root element."),
}
}
fn root_element_to_tokens(
cx: &Ident,
template_uid: &Ident,
node: &NodeElement,
) -> TokenStream {
let mut template = String::new();
let mut navigations = Vec::new();
let mut expressions = Vec::new();
if is_component_node(node) {
crate::view::component_to_tokens(cx, node, None)
} else {
element_to_tokens(
cx,
node,
&Ident::new("root", Span::call_site()),
None,
&mut 0,
&mut 0,
&mut template,
&mut navigations,
&mut expressions,
true,
);
// create the root element from which navigations and expressions will begin
let generate_root = quote! {
let root = #template_uid.with(|tpl| tpl.content().clone_node_with_deep(true))
.unwrap()
.first_child()
.unwrap();
};
let span = node.name.span();
let navigations = if navigations.is_empty() {
quote! {}
} else {
quote! { #(#navigations);* }
};
let expressions = if expressions.is_empty() {
quote! {}
} else {
quote! { #(#expressions;);* }
};
let tag_name = node.name.to_string();
quote_spanned! {
span => {
thread_local! {
static #template_uid: web_sys::HtmlTemplateElement = {
let document = leptos::document();
let el = document.create_element("template").unwrap();
el.set_inner_html(#template);
el.unchecked_into()
};
}
#generate_root
#navigations
#expressions
leptos::leptos_dom::View::Element(leptos::leptos_dom::Element {
#[cfg(debug_assertions)]
name: #tag_name.into(),
element: root.unchecked_into()
})
}
}
}
}
#[derive(Clone, Debug)]
enum PrevSibChange {
Sib(Ident),
Parent,
Skip,
}
fn attributes(node: &NodeElement) -> impl Iterator<Item = &NodeAttribute> {
node.attributes.iter().filter_map(|node| {
if let Node::Attribute(attribute) = node {
Some(attribute)
} else {
None
}
})
}
#[allow(clippy::too_many_arguments)]
fn element_to_tokens(
cx: &Ident,
node: &NodeElement,
parent: &Ident,
prev_sib: Option<Ident>,
next_el_id: &mut usize,
next_co_id: &mut usize,
template: &mut String,
navigations: &mut Vec<TokenStream>,
expressions: &mut Vec<TokenStream>,
is_root_el: bool,
) -> Ident {
// create this element
*next_el_id += 1;
let this_el_ident = child_ident(*next_el_id, node.name.span());
// Open tag
let name_str = node.name.to_string();
let span = node.name.span();
// CSR/hydrate, push to template
template.push('<');
template.push_str(&name_str);
// attributes
for attr in attributes(node) {
attr_to_tokens(cx, attr, &this_el_ident, template, expressions);
}
// navigation for this el
let debug_name = node.name.to_string();
let this_nav = if is_root_el {
quote_spanned! {
span => let #this_el_ident = #debug_name;
let #this_el_ident = #parent.clone().unchecked_into::<web_sys::Node>();
//debug!("=> got {}", #this_el_ident.node_name());
}
} else if let Some(prev_sib) = &prev_sib {
quote_spanned! {
span => let #this_el_ident = #debug_name;
//log::debug!("next_sibling ({})", #debug_name);
let #this_el_ident = #prev_sib.next_sibling().unwrap_or_else(|| panic!("error : {} => {} ", #debug_name, "nextSibling"));
//log::debug!("=> got {}", #this_el_ident.node_name());
}
} else {
quote_spanned! {
span => let #this_el_ident = #debug_name;
//log::debug!("first_child ({})", #debug_name);
let #this_el_ident = #parent.first_child().unwrap_or_else(|| panic!("error: {} => {}", #debug_name, "firstChild"));
//log::debug!("=> got {}", #this_el_ident.node_name());
}
};
navigations.push(this_nav);
// self-closing tags
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
if matches!(
name_str.as_str(),
"area"
| "base"
| "br"
| "col"
| "embed"
| "hr"
| "img"
| "input"
| "link"
| "meta"
| "param"
| "source"
| "track"
| "wbr"
) {
template.push_str("/>");
return this_el_ident;
} else {
template.push('>');
}
// iterate over children
let mut prev_sib = prev_sib;
for (idx, child) in node.children.iter().enumerate() {
// set next sib (for any insertions)
let next_sib = next_sibling_node(&node.children, idx + 1, next_el_id);
let curr_id = child_to_tokens(
cx,
child,
&this_el_ident,
if idx == 0 { None } else { prev_sib.clone() },
next_sib,
next_el_id,
next_co_id,
template,
navigations,
expressions,
);
prev_sib = match curr_id {
PrevSibChange::Sib(id) => Some(id),
PrevSibChange::Parent => None,
PrevSibChange::Skip => prev_sib,
};
}
// close tag
template.push_str("</");
template.push_str(&name_str);
template.push('>');
this_el_ident
}
fn next_sibling_node(
children: &[Node],
idx: usize,
next_el_id: &mut usize,
) -> Option<Ident> {
if children.len() <= idx {
None
} else {
let sibling = &children[idx];
match sibling {
Node::Element(sibling) => {
if is_component_node(sibling) {
next_sibling_node(children, idx + 1, next_el_id)
} else {
Some(child_ident(*next_el_id + 1, sibling.name.span()))
}
}
Node::Block(sibling) => {
Some(child_ident(*next_el_id + 1, sibling.value.span()))
}
Node::Text(sibling) => {
Some(child_ident(*next_el_id + 1, sibling.value.span()))
}
_ => panic!("expected either an element or a block"),
}
}
}
fn attr_to_tokens(
cx: &Ident,
node: &NodeAttribute,
el_id: &Ident,
template: &mut String,
expressions: &mut Vec<TokenStream>,
) {
let name = node.key.to_string();
let name = if name.starts_with('_') {
name.replacen('_', "", 1)
} else {
name
};
let name = if name.starts_with("attr:") {
name.replacen("attr:", "", 1)
} else {
name
};
let value = match &node.value {
Some(expr) => match expr.as_ref() {
syn::Expr::Lit(expr_lit) => {
if let syn::Lit::Str(s) = &expr_lit.lit {
AttributeValue::Static(s.value())
} else {
AttributeValue::Dynamic(expr)
}
}
_ => AttributeValue::Dynamic(expr),
},
None => AttributeValue::Empty,
};
let span = node.key.span();
// refs
if name == "ref" {
panic!("node_ref not yet supported in template! macro")
}
// Event Handlers
else if name.starts_with("on:") {
let (event_type, handler) =
crate::view::event_from_attribute_node(node, false);
expressions.push(quote! {
leptos::leptos_dom::add_event_helper(#el_id.unchecked_ref(), #event_type, #handler);
})
}
// Properties
else if name.starts_with("prop:") {
let name = name.replacen("prop:", "", 1);
let value = node
.value
.as_ref()
.expect("prop: blocks need values")
.as_ref();
expressions.push(quote_spanned! {
span => leptos_dom::property(#cx, #el_id.unchecked_ref(), #name, #value.into_property(#cx))
});
}
// Classes
else if name.starts_with("class:") {
let name = name.replacen("class:", "", 1);
let value = node
.value
.as_ref()
.expect("class: attributes need values")
.as_ref();
expressions.push(quote_spanned! {
span => leptos::leptos_dom::class_helper(#el_id.unchecked_ref(), #name.into(), #value.into_class(#cx))
});
}
// Attributes
else {
match value {
AttributeValue::Empty => {
template.push(' ');
template.push_str(&name);
}
// Static attributes (i.e., just a literal given as value, not an expression)
// are just set in the template — again, nothing programmatic
AttributeValue::Static(value) => {
template.push(' ');
template.push_str(&name);
template.push_str("=\"");
template.push_str(&value);
template.push('"');
}
AttributeValue::Dynamic(value) => {
// For client-side rendering, dynamic attributes don't need to be rendered in the template
// They'll immediately be set synchronously before the cloned template is mounted
expressions.push(quote_spanned! {
span => leptos::leptos_dom::attribute_helper(#el_id.unchecked_ref(), #name.into(), {#value}.into_attribute(#cx))
});
}
}
}
}
enum AttributeValue<'a> {
Static(String),
Dynamic(&'a syn::Expr),
Empty,
}
#[allow(clippy::too_many_arguments)]
fn child_to_tokens(
cx: &Ident,
node: &Node,
parent: &Ident,
prev_sib: Option<Ident>,
next_sib: Option<Ident>,
next_el_id: &mut usize,
next_co_id: &mut usize,
template: &mut String,
navigations: &mut Vec<TokenStream>,
expressions: &mut Vec<TokenStream>,
) -> PrevSibChange {
match node {
Node::Element(node) => {
if is_component_node(node) {
proc_macro_error::emit_error!(
node.name.span(),
"component children not allowed in template!, use view! \
instead"
);
PrevSibChange::Skip
} else {
PrevSibChange::Sib(element_to_tokens(
cx,
node,
parent,
prev_sib,
next_el_id,
next_co_id,
template,
navigations,
expressions,
false,
))
}
}
Node::Text(node) => block_to_tokens(
cx,
&node.value,
node.value.span(),
parent,
prev_sib,
next_sib,
next_el_id,
template,
expressions,
navigations,
),
Node::Block(node) => block_to_tokens(
cx,
&node.value,
node.value.span(),
parent,
prev_sib,
next_sib,
next_el_id,
template,
expressions,
navigations,
),
_ => panic!("unexpected child node type"),
}
}
#[allow(clippy::too_many_arguments)]
fn block_to_tokens(
_cx: &Ident,
value: &NodeValueExpr,
span: Span,
parent: &Ident,
prev_sib: Option<Ident>,
next_sib: Option<Ident>,
next_el_id: &mut usize,
template: &mut String,
expressions: &mut Vec<TokenStream>,
navigations: &mut Vec<TokenStream>,
) -> PrevSibChange {
let value = value.as_ref();
let str_value = match value {
syn::Expr::Lit(lit) => match &lit.lit {
syn::Lit::Str(s) => Some(s.value()),
syn::Lit::Char(c) => Some(c.value().to_string()),
syn::Lit::Int(i) => Some(i.base10_digits().to_string()),
syn::Lit::Float(f) => Some(f.base10_digits().to_string()),
_ => None,
},
_ => None,
};
// code to navigate to this text node
let (name, location) = /* if is_first_child && mode == Mode::Client {
(None, quote! { })
}
else */ {
*next_el_id += 1;
let name = child_ident(*next_el_id, span);
let location = if let Some(sibling) = &prev_sib {
quote_spanned! {
span => //log::debug!("-> next sibling");
let #name = #sibling.next_sibling().unwrap_or_else(|| panic!("error : {} => {} ", "{block}", "nextSibling"));
//log::debug!("\tnext sibling = {}", #name.node_name());
}
} else {
quote_spanned! {
span => //log::debug!("\\|/ first child on {}", #parent.node_name());
let #name = #parent.first_child().unwrap_or_else(|| panic!("error : {} => {} ", "{block}", "firstChild"));
//log::debug!("\tfirst child = {}", #name.node_name());
}
};
(Some(name), location)
};
let mount_kind = match &next_sib {
Some(child) => {
quote! { leptos::leptos_dom::MountKind::Before(#child.clone()) }
}
None => {
quote! { leptos::leptos_dom::MountKind::Append(&#parent) }
}
};
if let Some(v) = str_value {
navigations.push(location);
template.push_str(&v);
if let Some(name) = name {
PrevSibChange::Sib(name)
} else {
PrevSibChange::Parent
}
} else {
template.push_str("<!>");
navigations.push(location);
expressions.push(quote! {
leptos::leptos_dom::mount_child(#mount_kind, &{#value}.into_view(cx));
});
if let Some(name) = name {
PrevSibChange::Sib(name)
} else {
PrevSibChange::Parent
}
}
}
fn child_ident(el_id: usize, span: Span) -> Ident {
let id = format!("_el{el_id}");
Ident::new(&id, span)
}

View File

@ -832,7 +832,7 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
.expect("couldn't parse event name");
let event_type = if is_custom {
quote! { leptos::leptos_dom::leptos_dom::events::Custom::new(#name) }
quote! { leptos::ev::Custom::new(#name) }
} else {
event_type
};
@ -887,9 +887,9 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
} else {
quote! { undelegated }
};
quote! { ::leptos::leptos_dom::ev::#undelegated(::leptos::leptos_dom::ev::#event_type) }
quote! { ::leptos::ev::#undelegated(::leptos::ev::#event_type) }
} else {
quote! { ::leptos::leptos_dom::ev::#event_type }
quote! { ::leptos::ev::#event_type }
};
quote! {
@ -969,7 +969,7 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
}
}
fn component_to_tokens(
pub(crate) fn component_to_tokens(
cx: &Ident,
node: &NodeElement,
global_class: Option<&TokenTree>,
@ -1081,7 +1081,7 @@ fn component_to_tokens(
}
}
fn event_from_attribute_node(
pub(crate) fn event_from_attribute_node(
attr: &NodeAttribute,
force_undelegated: bool,
) -> (TokenStream, &Expr) {