diff --git a/examples/js-framework-benchmark/src/lib.rs b/examples/js-framework-benchmark/src/lib.rs
index 2985ef040..db7ded721 100644
--- a/examples/js-framework-benchmark/src/lib.rs
+++ b/examples/js-framework-benchmark/src/lib.rs
@@ -162,22 +162,24 @@ pub fn App() -> impl IntoView {
- {row_id.to_string()} |
- {move || label.get()} |
- |
- |
-
- })
+ template! {
+ < tr class : danger = { move || is_selected.selected(Some(row_id)) }
+ > < td class = "col-md-1" > { row_id.to_string() } td > < td
+ class = "col-md-4" >< a on : click = move | _ | set_selected
+ .set(Some(row_id)) > { move || label.get() } a > td > < td
+ class = "col-md-1" >< a on : click = move | _ | remove(row_id) ><
+ span class = "glyphicon glyphicon-remove" aria - hidden = "true" >
+ span > a > td > < td class = "col-md-6" /> tr >
+ }
}
/>
+
diff --git a/leptos_macro/src/lib.rs b/leptos_macro/src/lib.rs
index 8abc3667e..325d1f33f 100644
--- a/leptos_macro/src/lib.rs
+++ b/leptos_macro/src/lib.rs
@@ -266,6 +266,21 @@ mod slot;
#[proc_macro]
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
pub fn view(tokens: TokenStream) -> TokenStream {
+ view_macro_impl(tokens, false)
+}
+
+/// The `template` macro behaves like [`view`], except that it wraps the entire tree in a
+/// [`ViewTemplate`](leptos::prelude::ViewTemplate). This optimizes creation speed by rendering
+/// most of the view into a `` tag with HTML rendered at compile time, then hydrating it.
+/// In exchange, there is a small binary size overhead.
+#[proc_macro_error2::proc_macro_error]
+#[proc_macro]
+#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
+pub fn template(tokens: TokenStream) -> TokenStream {
+ view_macro_impl(tokens, true)
+}
+
+fn view_macro_impl(tokens: TokenStream, template: bool) -> TokenStream {
let tokens: proc_macro2::TokenStream = tokens.into();
let mut tokens = tokens.into_iter();
@@ -308,12 +323,13 @@ pub fn view(tokens: TokenStream) -> TokenStream {
&mut nodes,
global_class.as_ref(),
normalized_call_site(proc_macro::Span::call_site()),
+ template,
);
// The allow lint needs to be put here instead of at the expansion of
// view::attribute_value(). Adding this next to the expanded expression
// seems to break rust-analyzer, but it works when the allow is put here.
- quote! {
+ let output = quote! {
{
#[allow(unused_braces)]
{
@@ -321,6 +337,14 @@ pub fn view(tokens: TokenStream) -> TokenStream {
#nodes_output
}
}
+ };
+
+ if template {
+ quote! {
+ ::leptos::prelude::ViewTemplate::new(#output)
+ }
+ } else {
+ output
}
.into()
}
diff --git a/leptos_macro/src/view/component_builder.rs b/leptos_macro/src/view/component_builder.rs
index 9ee0c8ddc..09dac5ab9 100644
--- a/leptos_macro/src/view/component_builder.rs
+++ b/leptos_macro/src/view/component_builder.rs
@@ -12,6 +12,7 @@ use syn::{spanned::Spanned, Expr, ExprPath, ExprRange, RangeLimits, Stmt};
pub(crate) fn component_to_tokens(
node: &mut NodeElement,
global_class: Option<&TokenTree>,
+ disable_inert_html: bool,
) -> TokenStream {
#[allow(unused)] // TODO this is used by hot-reloading
#[cfg(debug_assertions)]
@@ -191,6 +192,7 @@ pub(crate) fn component_to_tokens(
Some(&mut slots),
global_class,
None,
+ disable_inert_html,
);
// TODO view marker for hot-reloading
diff --git a/leptos_macro/src/view/mod.rs b/leptos_macro/src/view/mod.rs
index 1aa022d23..657a31e95 100644
--- a/leptos_macro/src/view/mod.rs
+++ b/leptos_macro/src/view/mod.rs
@@ -15,7 +15,7 @@ use rstml::node::{
};
use std::{
cmp::Ordering,
- collections::{HashMap, HashSet},
+ collections::{HashMap, HashSet, VecDeque},
};
use syn::{
spanned::Spanned, Expr, Expr::Tuple, ExprLit, ExprRange, Lit, LitStr,
@@ -34,6 +34,7 @@ pub fn render_view(
nodes: &mut [Node],
global_class: Option<&TokenTree>,
view_marker: Option,
+ disable_inert_html: bool,
) -> Option {
let (base, should_add_view) = match nodes.len() {
0 => {
@@ -52,6 +53,8 @@ pub fn render_view(
None,
global_class,
view_marker.as_deref(),
+ true,
+ disable_inert_html,
),
// only add View wrapper and view marker to a regular HTML
// element or component, not to a <{..} /> attribute list
@@ -67,6 +70,7 @@ pub fn render_view(
None,
global_class,
view_marker.as_deref(),
+ disable_inert_html,
),
true,
),
@@ -91,12 +95,281 @@ pub fn render_view(
})
}
+fn is_inert_element(orig_node: &Node) -> bool {
+ // do not use this if the top-level node is not an Element,
+ // or if it's an element with no children and no attrs
+ match orig_node {
+ Node::Element(el) => {
+ if el.attributes().is_empty() && el.children.is_empty() {
+ return false;
+ }
+ }
+ _ => return false,
+ }
+
+ // otherwise, walk over all the nodes to make sure everything is inert
+ let mut nodes = VecDeque::from([orig_node]);
+
+ while let Some(current_element) = nodes.pop_front() {
+ match current_element {
+ Node::Text(_) | Node::RawText(_) => {}
+ Node::Element(node) => {
+ if is_component_node(node) {
+ return false;
+ }
+ if is_spread_marker(node) {
+ return false;
+ }
+
+ match node.name() {
+ NodeName::Block(_) => return false,
+ _ => {
+ // check all attributes
+ for attr in node.attributes() {
+ match attr {
+ NodeAttribute::Block(_) => return false,
+ NodeAttribute::Attribute(attr) => {
+ let static_key =
+ !matches!(attr.key, NodeName::Block(_));
+
+ let static_value = match attr
+ .possible_value
+ .to_value()
+ {
+ None => true,
+ Some(value) => {
+ matches!(&value.value, KVAttributeValue::Expr(expr) if {
+ if let Expr::Lit(lit) = expr {
+ matches!(&lit.lit, Lit::Str(_))
+ } else {
+ false
+ }
+ })
+ }
+ };
+
+ if !static_key || !static_value {
+ return false;
+ }
+ }
+ }
+ }
+
+ // check all children
+ nodes.extend(&node.children);
+ }
+ }
+ }
+ _ => return false,
+ }
+ }
+
+ true
+}
+
+enum Item<'a, T> {
+ Node(&'a Node),
+ ClosingTag(String),
+}
+
+enum InertElementBuilder<'a> {
+ GlobalClass {
+ global_class: &'a TokenTree,
+ strs: Vec>,
+ buffer: String,
+ },
+ NoGlobalClass {
+ buffer: String,
+ },
+}
+
+impl<'a> ToTokens for InertElementBuilder<'a> {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ match self {
+ InertElementBuilder::GlobalClass { strs, .. } => {
+ tokens.extend(quote! {
+ [#(#strs),*].join("")
+ });
+ }
+ InertElementBuilder::NoGlobalClass { buffer } => {
+ tokens.extend(quote! {
+ #buffer
+ })
+ }
+ }
+ }
+}
+
+enum GlobalClassItem<'a> {
+ Global(&'a TokenTree),
+ String(String),
+}
+
+impl<'a> ToTokens for GlobalClassItem<'a> {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ let addl_tokens = match self {
+ GlobalClassItem::Global(v) => v.to_token_stream(),
+ GlobalClassItem::String(v) => v.to_token_stream(),
+ };
+ tokens.extend(addl_tokens);
+ }
+}
+
+impl<'a> InertElementBuilder<'a> {
+ fn new(global_class: Option<&'a TokenTree>) -> Self {
+ match global_class {
+ None => Self::NoGlobalClass {
+ buffer: String::new(),
+ },
+ Some(global_class) => Self::GlobalClass {
+ global_class,
+ strs: Vec::new(),
+ buffer: String::new(),
+ },
+ }
+ }
+
+ fn push(&mut self, c: char) {
+ match self {
+ InertElementBuilder::GlobalClass { buffer, .. } => buffer.push(c),
+ InertElementBuilder::NoGlobalClass { buffer } => buffer.push(c),
+ }
+ }
+
+ fn push_str(&mut self, s: &str) {
+ match self {
+ InertElementBuilder::GlobalClass { buffer, .. } => {
+ buffer.push_str(s)
+ }
+ InertElementBuilder::NoGlobalClass { buffer } => buffer.push_str(s),
+ }
+ }
+
+ fn push_class(&mut self, class: &str) {
+ match self {
+ InertElementBuilder::GlobalClass {
+ global_class,
+ strs,
+ buffer,
+ } => {
+ buffer.push_str(" class=\"");
+ strs.push(GlobalClassItem::String(std::mem::take(buffer)));
+ strs.push(GlobalClassItem::Global(global_class));
+ buffer.push(' ');
+ buffer.push_str(class);
+ buffer.push('"');
+ }
+ InertElementBuilder::NoGlobalClass { buffer } => {
+ buffer.push_str(" class=\"");
+ buffer.push_str(class);
+ buffer.push('"');
+ }
+ }
+ }
+
+ fn finish(&mut self) {
+ match self {
+ InertElementBuilder::GlobalClass { strs, buffer, .. } => {
+ strs.push(GlobalClassItem::String(std::mem::take(buffer)));
+ }
+ InertElementBuilder::NoGlobalClass { .. } => {}
+ }
+ }
+}
+
+fn inert_element_to_tokens(
+ node: &Node,
+ global_class: Option<&TokenTree>,
+) -> Option {
+ let mut html = InertElementBuilder::new(global_class);
+ let mut nodes = VecDeque::from([Item::Node(node)]);
+
+ while let Some(current) = nodes.pop_front() {
+ match current {
+ Item::ClosingTag(tag) => {
+ // closing tag
+ html.push_str("");
+ html.push_str(&tag);
+ html.push('>');
+ }
+ Item::Node(current) => {
+ match current {
+ Node::RawText(raw) => {
+ let text = raw.to_string_best();
+ html.push_str(&text);
+ }
+ Node::Text(text) => {
+ let text = text.value_string();
+ html.push_str(&text);
+ }
+ Node::Element(node) => {
+ let self_closing = is_self_closing(node);
+ let el_name = node.name().to_string();
+
+ // opening tag
+ html.push('<');
+ html.push_str(&el_name);
+
+ for attr in node.attributes() {
+ if let NodeAttribute::Attribute(attr) = attr {
+ let attr_name = attr.key.to_string();
+ if attr_name != "class" {
+ html.push(' ');
+ html.push_str(&attr_name);
+ }
+
+ if let Some(value) =
+ attr.possible_value.to_value()
+ {
+ if let KVAttributeValue::Expr(Expr::Lit(
+ lit,
+ )) = &value.value
+ {
+ if let Lit::Str(txt) = &lit.lit {
+ if attr_name == "class" {
+ html.push_class(&txt.value());
+ } else {
+ html.push_str("=\"");
+ html.push_str(&txt.value());
+ html.push('"');
+ }
+ }
+ }
+ };
+ }
+ }
+
+ html.push('>');
+
+ // render all children
+ if !self_closing {
+ nodes.push_front(Item::ClosingTag(el_name));
+ let children = node.children.iter().rev();
+ for child in children {
+ nodes.push_front(Item::Node(child));
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+ }
+
+ html.finish();
+
+ Some(quote! {
+ ::leptos::tachys::html::InertElement::new(#html)
+ })
+}
+
fn element_children_to_tokens(
nodes: &mut [Node],
parent_type: TagType,
parent_slots: Option<&mut HashMap>>,
global_class: Option<&TokenTree>,
view_marker: Option<&str>,
+ disable_inert_html: bool,
) -> Option {
let children = children_to_tokens(
nodes,
@@ -104,6 +377,8 @@ fn element_children_to_tokens(
parent_slots,
global_class,
view_marker,
+ false,
+ disable_inert_html,
);
if children.is_empty() {
None
@@ -145,6 +420,7 @@ fn fragment_to_tokens(
parent_slots: Option<&mut HashMap>>,
global_class: Option<&TokenTree>,
view_marker: Option<&str>,
+ disable_inert_html: bool,
) -> Option {
let children = children_to_tokens(
nodes,
@@ -152,6 +428,8 @@ fn fragment_to_tokens(
parent_slots,
global_class,
view_marker,
+ true,
+ disable_inert_html,
);
if children.is_empty() {
None
@@ -183,6 +461,8 @@ fn children_to_tokens(
parent_slots: Option<&mut HashMap>>,
global_class: Option<&TokenTree>,
view_marker: Option<&str>,
+ top_level: bool,
+ disable_inert_html: bool,
) -> Vec {
if nodes.len() == 1 {
match node_to_tokens(
@@ -191,6 +471,8 @@ fn children_to_tokens(
parent_slots,
global_class,
view_marker,
+ top_level,
+ disable_inert_html,
) {
Some(tokens) => vec![tokens],
None => vec![],
@@ -206,6 +488,8 @@ fn children_to_tokens(
Some(&mut slots),
global_class,
view_marker,
+ top_level,
+ disable_inert_html,
)
})
.collect();
@@ -227,7 +511,11 @@ fn node_to_tokens(
parent_slots: Option<&mut HashMap>>,
global_class: Option<&TokenTree>,
view_marker: Option<&str>,
+ top_level: bool,
+ disable_inert_html: bool,
) -> Option {
+ let is_inert = !disable_inert_html && is_inert_element(node);
+
match node {
Node::Comment(_) => None,
Node::Doctype(node) => {
@@ -240,6 +528,7 @@ fn node_to_tokens(
parent_slots,
global_class,
view_marker,
+ disable_inert_html,
),
Node::Block(block) => Some(quote! { #block }),
Node::Text(text) => Some(text_to_tokens(&text.value)),
@@ -248,13 +537,20 @@ fn node_to_tokens(
let text = syn::LitStr::new(&text, raw.span());
Some(text_to_tokens(&text))
}
- Node::Element(node) => element_to_tokens(
- node,
- parent_type,
- parent_slots,
- global_class,
- view_marker,
- ),
+ Node::Element(el_node) => {
+ if !top_level && is_inert {
+ inert_element_to_tokens(node, global_class)
+ } else {
+ element_to_tokens(
+ el_node,
+ parent_type,
+ parent_slots,
+ global_class,
+ view_marker,
+ disable_inert_html,
+ )
+ }
+ }
Node::Custom(node) => Some(node.to_token_stream()),
}
}
@@ -278,6 +574,7 @@ pub(crate) fn element_to_tokens(
parent_slots: Option<&mut HashMap>>,
global_class: Option<&TokenTree>,
view_marker: Option<&str>,
+ disable_inert_html: bool,
) -> Option {
// attribute sorting:
//
@@ -347,10 +644,16 @@ pub(crate) fn element_to_tokens(
if is_component_node(node) {
if let Some(slot) = get_slot(node) {
let slot = slot.clone();
- slot_to_tokens(node, &slot, parent_slots, global_class);
+ slot_to_tokens(
+ node,
+ &slot,
+ parent_slots,
+ global_class,
+ disable_inert_html,
+ );
None
} else {
- Some(component_to_tokens(node, global_class))
+ Some(component_to_tokens(node, global_class, disable_inert_html))
}
} else if is_spread_marker(node) {
let mut attributes = Vec::new();
@@ -467,6 +770,7 @@ pub(crate) fn element_to_tokens(
parent_slots,
global_class,
view_marker,
+ disable_inert_html,
)
} else {
if !node.children.is_empty() {
diff --git a/leptos_macro/src/view/slot_helper.rs b/leptos_macro/src/view/slot_helper.rs
index 7c570e301..e579e0972 100644
--- a/leptos_macro/src/view/slot_helper.rs
+++ b/leptos_macro/src/view/slot_helper.rs
@@ -11,6 +11,7 @@ pub(crate) fn slot_to_tokens(
slot: &KeyedAttribute,
parent_slots: Option<&mut HashMap>>,
global_class: Option<&TokenTree>,
+ disable_inert_html: bool,
) {
let name = slot.key.to_string();
let name = name.trim();
@@ -118,6 +119,7 @@ pub(crate) fn slot_to_tokens(
Some(&mut slots),
global_class,
None,
+ disable_inert_html,
);
// TODO view markers for hot-reloading
diff --git a/tachys/src/html/mod.rs b/tachys/src/html/mod.rs
index df5874ab3..d63d673a8 100644
--- a/tachys/src/html/mod.rs
+++ b/tachys/src/html/mod.rs
@@ -1,9 +1,12 @@
+use self::attribute::Attribute;
use crate::{
+ hydration::Cursor,
no_attrs,
- renderer::Renderer,
- view::{Position, Render, RenderHtml},
+ prelude::AddAnyAttr,
+ renderer::{CastFrom, DomRenderer, Renderer},
+ view::{Position, PositionState, Render, RenderHtml},
};
-use std::marker::PhantomData;
+use std::{borrow::Cow, marker::PhantomData};
/// Types for HTML attributes.
pub mod attribute;
@@ -76,8 +79,99 @@ where
fn hydrate(
self,
- _cursor: &crate::hydration::Cursor,
- _position: &crate::view::PositionState,
+ _cursor: &Cursor,
+ _position: &PositionState,
) -> Self::State {
}
}
+
+/// An element that contains no interactivity, and whose contents can be known at compile time.
+pub struct InertElement {
+ html: Cow<'static, str>,
+}
+
+impl InertElement {
+ /// Creates a new inert element.
+ pub fn new(html: impl Into>) -> Self {
+ Self { html: html.into() }
+ }
+}
+
+impl Render for InertElement
+where
+ Rndr: DomRenderer,
+{
+ type State = Rndr::Element;
+
+ fn build(self) -> Self::State {
+ Rndr::create_element_from_html(&self.html)
+ }
+
+ fn rebuild(self, _state: &mut Self::State) {}
+}
+
+impl AddAnyAttr for InertElement
+where
+ Rndr: DomRenderer,
+{
+ type Output> = Self;
+
+ fn add_any_attr>(
+ self,
+ _attr: NewAttr,
+ ) -> Self::Output
+ where
+ Self::Output: RenderHtml,
+ {
+ panic!(
+ "InertElement does not support adding attributes. It should only \
+ be used as a child, and not returned at the top level."
+ )
+ }
+}
+
+impl RenderHtml for InertElement
+where
+ Rndr: DomRenderer,
+{
+ type AsyncOutput = Self;
+
+ const MIN_LENGTH: usize = 0;
+
+ fn html_len(&self) -> usize {
+ self.html.len()
+ }
+
+ fn dry_resolve(&mut self) {}
+
+ async fn resolve(self) -> Self {
+ self
+ }
+
+ fn to_html_with_buf(
+ self,
+ buf: &mut String,
+ position: &mut Position,
+ _escape: bool,
+ _mark_branches: bool,
+ ) {
+ buf.push_str(&self.html);
+ *position = Position::NextChild;
+ }
+
+ fn hydrate(
+ self,
+ cursor: &Cursor,
+ position: &PositionState,
+ ) -> Self::State {
+ let curr_position = position.get();
+ if curr_position == Position::FirstChild {
+ cursor.child();
+ } else if curr_position != Position::Current {
+ cursor.sibling();
+ }
+ let el = Rndr::Element::cast_from(cursor.current()).unwrap();
+ position.set(Position::NextChild);
+ el
+ }
+}
diff --git a/tachys/src/renderer/dom.rs b/tachys/src/renderer/dom.rs
index 177b5ac20..bfc69d926 100644
--- a/tachys/src/renderer/dom.rs
+++ b/tachys/src/renderer/dom.rs
@@ -390,6 +390,13 @@ impl DomRenderer for Dom {
.unwrap()
.unchecked_into()
}
+
+ fn create_element_from_html(html: &str) -> Self::Element {
+ // TODO can be optimized to cache HTML strings or cache ?
+ let tpl = document().create_element("template").unwrap();
+ tpl.set_inner_html(html);
+ Self::clone_template(tpl.unchecked_ref())
+ }
}
impl Mountable for Node {
diff --git a/tachys/src/renderer/mock_dom.rs b/tachys/src/renderer/mock_dom.rs
index eb3ee8493..4a67e4085 100644
--- a/tachys/src/renderer/mock_dom.rs
+++ b/tachys/src/renderer/mock_dom.rs
@@ -300,6 +300,10 @@ impl DomRenderer for MockDom {
fn clone_template(tpl: &Self::TemplateElement) -> Self::Element {
todo!()
}
+
+ fn create_element_from_html(html: &str) -> Self::Element {
+ todo!()
+ }
}
impl Default for Document {
diff --git a/tachys/src/renderer/mod.rs b/tachys/src/renderer/mod.rs
index afaaea185..7fcc62320 100644
--- a/tachys/src/renderer/mod.rs
+++ b/tachys/src/renderer/mod.rs
@@ -213,8 +213,12 @@ pub trait DomRenderer: Renderer {
fn get_template() -> Self::TemplateElement
where
V: ToTemplate + 'static;
+
/// Deeply clones a template.
fn clone_template(tpl: &Self::TemplateElement) -> Self::Element;
+
+ /// Creates a single element from a string of HTML.
+ fn create_element_from_html(html: &str) -> Self::Element;
}
/// Attempts to cast from one type to another.
diff --git a/tachys/src/renderer/sledgehammer.rs b/tachys/src/renderer/sledgehammer.rs
index 91b809bf9..9082f925f 100644
--- a/tachys/src/renderer/sledgehammer.rs
+++ b/tachys/src/renderer/sledgehammer.rs
@@ -560,6 +560,10 @@ impl DomRenderer for Sledgehammer {
});
node
}
+
+ fn create_element_from_html(html: &str) -> Self::Element {
+ todo!()
+ }
}
impl Mountable for SNode {