Add <Stylesheet> component to meta

This commit is contained in:
Greg Johnston 2022-09-28 20:54:56 -04:00
parent 6365ac2c8c
commit b2fcb552ea
14 changed files with 293 additions and 111 deletions

View file

@ -19,6 +19,6 @@ pub fn main() {
Todo::new(cx, 2, "Profit!".to_string()),
]);
view! { <main><TodoMVC todos=todos/></main> }
view! { <TodoMVC todos=todos/> }
});
}

View file

@ -31,9 +31,7 @@ async fn render_todomvc() -> impl Responder {
]);
view! {
<main>
<TodoMVC todos=todos/>
</main>
<TodoMVC todos=todos/>
}
}
})

View file

@ -19,7 +19,7 @@ lto = true
opt-level = 'z'
[features]
default = ["csr"]
default = ["hydrate"]
csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"]
ssr = ["leptos/ssr"]

View file

@ -109,7 +109,7 @@ const ESCAPE_KEY: u32 = 27;
const ENTER_KEY: u32 = 13;
#[component]
pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
let mut next_id = todos
.0
.iter()
@ -178,7 +178,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
});
view! {
<>
<main>
<section class="todoapp">
<header class="header">
<h1>"todos"</h1>
@ -225,7 +225,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
<p>"Created by "<a href="http://todomvc.com">"Greg Johnston"</a></p>
<p>"Part of "<a href="http://todomvc.com">"TodoMVC"</a></p>
</footer>
</>
</main>
}
}

View file

@ -2,7 +2,7 @@ use std::time::Duration;
use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};
use crate::{event_delegation, is_server};
use crate::{debug_warn, event_delegation, is_server};
thread_local! {
pub static WINDOW: web_sys::Window = web_sys::window().unwrap_throw();
@ -71,7 +71,31 @@ pub fn insert_before(
new: &web_sys::Node,
existing: Option<&web_sys::Node>,
) -> web_sys::Node {
parent.insert_before(new, existing).unwrap_throw()
log::warn!(
"insert_before insert {} before {:?} \n\non {}",
new.node_name(),
existing.map(|e| e.node_name()),
parent.node_name(),
);
if parent.node_type() != 1 {
debug_warn!("insert_before: trying to insert on a parent node that is not an element");
new.clone()
} else if let Some(existing) = existing {
if existing.parent_node().as_ref() == Some(parent.unchecked_ref()) {
match parent.insert_before(new, Some(existing)) {
Ok(c) => c,
Err(e) => {
log::warn!("{:?}", e.as_string());
new.clone()
}
}
} else {
debug_warn!("insert_before: existing node is not a child of parent node");
parent.append_child(new).unwrap_throw()
}
} else {
parent.append_child(new).unwrap_throw()
}
}
pub fn replace_with(old_node: &web_sys::Element, new_node: &web_sys::Node) {

View file

@ -26,9 +26,10 @@ pub fn reconcile_arrays(parent: &web_sys::Element, a: &mut [web_sys::Node], b: &
{
for (i, node) in a.iter().enumerate() {
if node.parent_node().as_ref() != Some(parent) {
panic!(
crate::debug_warn!(
"node {} in existing nodes Vec is not a child of parent. node = {:#?}",
i, node
i,
node
);
}
}
@ -81,8 +82,8 @@ pub fn reconcile_arrays(parent: &web_sys::Element, a: &mut [web_sys::Node], b: &
} else if a[a_start] == b[b_end - 1] && b[b_start] == a[a_end - 1] {
// Swap backwards.
let node = a[a_end - 1].next_sibling();
parent.insert_before(&b[b_start], a[a_start].next_sibling().as_ref());
parent.insert_before(&b[b_end - 1], node.as_ref());
insert_before(parent, &b[b_start], a[a_start].next_sibling().as_ref());
insert_before(parent, &b[b_end - 1], node.as_ref());
a_start += 1;
b_start += 1;
a_end -= 1;
@ -119,7 +120,7 @@ pub fn reconcile_arrays(parent: &web_sys::Element, a: &mut [web_sys::Node], b: &
if sequence > index - b_start {
let node = &a[a_start];
while b_start < index {
parent.insert_before(&b[b_start], Some(node));
insert_before(parent, &b[b_start], Some(node));
b_start += 1;
}
} else {
@ -142,7 +143,7 @@ pub fn reconcile_arrays(parent: &web_sys::Element, a: &mut [web_sys::Node], b: &
{
for (i, node) in b.iter().enumerate() {
if node.parent_node().as_ref() != Some(parent) {
panic!(
crate::debug_warn!(
"node {} in new nodes Vec is not a child of parent after reconciliation. node = {:#?}",
i, node
);

View file

@ -132,6 +132,12 @@ pub fn insert(
"inserting {value:?} on {} before {before:?} with initial = {initial:?}",
parent.node_name()
); */
let initial =
if before != Marker::NoChildren && (initial == None || initial == Some(Child::Null)) {
Some(Child::Nodes(vec![]))
} else {
initial
};
match value {
Child::Fn(f) => {
@ -148,6 +154,7 @@ pub fn insert(
}
Some(insert_expression(
cx,
parent.clone().unchecked_into(),
&value,
current,
@ -160,6 +167,7 @@ pub fn insert(
}
_ => {
insert_expression(
cx,
parent.unchecked_into(),
&value,
initial.unwrap_or(Child::Null),
@ -170,11 +178,19 @@ pub fn insert(
}
pub fn insert_expression(
cx: Scope,
parent: web_sys::Element,
new_value: &Child,
mut current: Child,
before: &Marker,
) -> Child {
#[cfg(feature = "hydrate")]
if cx.is_hydrating() && current == Child::Null {
current = Child::Nodes(child_nodes(&parent));
}
log::debug!("insert_expression\nparent = {}\nnew_value = {new_value:?}\ncurrent = {current:?}\nbefore = {before:?}", parent.node_name());
if new_value == &current {
current
} else {
@ -211,7 +227,11 @@ pub fn insert_expression(
}
Child::Null => match before {
Marker::BeforeChild(before) => {
Child::Node(insert_before(&parent, node, Some(before)))
if before.is_connected() {
Child::Node(insert_before(&parent, node, Some(before)))
} else {
Child::Node(append_child(&parent, node))
}
}
_ => Child::Node(append_child(&parent, node)),
},
@ -387,3 +407,14 @@ fn clean_children(
}
}
}
fn child_nodes(parent: &web_sys::Element) -> Vec<web_sys::Node> {
let children = parent.children();
let mut nodes = Vec::new();
for idx in 0..children.length() {
if let Some(node) = children.item(idx) {
nodes.push(node.clone().unchecked_into());
}
}
nodes
}

View file

@ -103,6 +103,7 @@ fn root_element_to_tokens(template_uid: &Ident, node: &Node, mode: Mode) -> Toke
// for hydration, use get_next_element(), which will either draw from an SSRed node or clone the template
Mode::Hydrate => quote! {
let root = #template_uid.with(|template| cx.get_next_element(template));
log::debug!("root node = {:#?}", root.outer_html());
},
};
@ -207,21 +208,21 @@ fn element_to_tokens(
quote_spanned! {
span => //let #this_el_ident = #debug_name;
let #this_el_ident = #parent.clone().unchecked_into::<web_sys::Node>();
//log::debug!("=> got {}", #this_el_ident.node_name());
log::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);
log::debug!("next_sibling ({})", #debug_name);
let #this_el_ident = #prev_sib.next_sibling().unwrap_throw();
//log::debug!("=> got {}", #this_el_ident.node_name());
log::debug!("=> got {}", #this_el_ident.node_name());
}
} else {
quote_spanned! {
span => //let #this_el_ident = #debug_name;
//log::debug!("first_child ({})", #debug_name);
log::debug!("first_child ({})", #debug_name);
let #this_el_ident = #parent.first_child().unwrap_throw();
//log::debug!("=> got {}", #this_el_ident.node_name());
log::debug!("=> got {}", #this_el_ident.node_name());
}
};
navigations.push(this_nav);
@ -465,7 +466,7 @@ fn attr_to_tokens(
(AttributeValue::Dynamic(value), Mode::Ssr) => {
expressions.push(quote_spanned! {
span => leptos_buffer.push(' ');
leptos_buffer.push_str(&#value.into_attribute(cx).as_value_string(#name));
leptos_buffer.push_str(&{#value}.into_attribute(cx).as_value_string(#name));
});
}
(AttributeValue::Dynamic(value), _) => {
@ -508,6 +509,9 @@ fn child_to_tokens(
next_sib,
template,
expressions,
navigations,
next_el_id,
next_co_id,
multi,
mode,
)
@ -605,7 +609,8 @@ fn child_to_tokens(
template.push_str("<!#><!/>");
navigations.push(quote! {
#location;
let (#el, #co) = cx.get_next_marker(&#name);
let (#el, #co) = cx.get_next_marker(&#parent);
log::debug!("get_next_marker => {} [{:?}]", #el.node_name(), #co.iter().map(|c| c.node_name()).collect::<Vec<_>>());
});
current = Some(co);
@ -645,9 +650,12 @@ fn child_to_tokens(
fn component_to_tokens(
node: &Node,
parent: Option<&Ident>,
next_sib: Option<Ident>,
mut next_sib: Option<Ident>,
template: &mut String,
expressions: &mut Vec<TokenStream>,
navigations: &mut Vec<TokenStream>,
next_el_id: &mut usize,
next_co_id: &mut usize,
multi: bool,
mode: Mode,
) -> PrevSibChange {
@ -669,11 +677,37 @@ fn component_to_tokens(
if mode == Mode::Ssr {
expressions.push(quote::quote_spanned! {
span => // TODO wrap components but use get_next_element() instead of first_child/next_sibling?
//leptos_buffer.push_str("<!--#-->");
leptos_buffer.push_str("<!--#-->");
leptos_buffer.push_str(&#create_component.into_child(cx).as_child_string());
//leptos_buffer.push_str("<!--/-->");
leptos_buffer.push_str("<!--/-->");
});
} else if mode == Mode::Hydrate {
let name = child_ident(*next_el_id, node);
*next_el_id += 1;
let el = child_ident(*next_el_id, node);
*next_co_id += 1;
let co = comment_ident(*next_co_id, node);
next_sib = Some(el.clone());
template.push_str("<!#><!/>");
navigations.push(quote! {
let (#el, #co) = cx.get_next_marker(&#name);
log::debug!("component get_next_marker => {} [{:?}]", #el.node_name(), #co.iter().map(|c| c.node_name()).collect::<Vec<_>>());
});
//current = Some(co);
expressions.push(quote! {
log::debug!("inserting! really!");
leptos::insert(
cx,
#el,
#create_component.into_child(cx),
Marker::NoChildren,
Some(Child::Nodes(#co)),
);
});
} else {
expressions.push(quote! {
leptos::insert(
@ -705,7 +739,7 @@ fn create_component(node: &Node, mode: Mode) -> TokenStream {
if mode == Mode::Hydrate {
(
quote_spanned! { span => let children = vec![#child]; },
quote_spanned! { span => .children(Box::new(move || children)) },
quote_spanned! { span => .children(Box::new(move || children.clone())) },
)
} else {
(

View file

@ -100,7 +100,7 @@ impl SharedContext {
completed: Default::default(),
events: Default::default(),
context: Some(HydrationContext {
id: "0-".into(),
id: "".into(),
count: -1,
}),
registry,
@ -111,8 +111,9 @@ impl SharedContext {
pub fn next_hydration_key(&mut self) -> String {
if let Some(context) = &mut self.context {
let k = format!("{}{}", context.id, context.count);
context.count += 1;
format!("{}{}", context.id, context.count)
k
} else {
self.context = Some(HydrationContext {
id: "0-".into(),

View file

@ -124,6 +124,11 @@ impl Scope {
}
}
#[cfg(feature = "hydrate")]
pub fn is_hydrating(&self) -> bool {
self.runtime.shared_context.borrow().is_some()
}
#[cfg(feature = "hydrate")]
pub fn start_hydration(&self, element: &web_sys::Element) {
self.runtime.start_hydration(element);

View file

@ -10,6 +10,7 @@ log = "0.4"
[dependencies.web-sys]
version = "0.3"
features = [
"HtmlLinkElement",
"HtmlTitleElement"
]

View file

@ -2,9 +2,15 @@ use std::{cell::RefCell, fmt::Debug, rc::Rc};
use leptos::*;
mod stylesheet;
mod title;
pub use stylesheet::*;
pub use title::*;
#[derive(Debug, Clone, Default)]
pub struct MetaContext {
title: TitleContext,
pub(crate) title: TitleContext,
pub(crate) stylesheets: StylesheetContext,
}
pub fn use_head(cx: Scope) -> MetaContext {
@ -21,18 +27,22 @@ impl MetaContext {
pub fn new() -> Self {
Default::default()
}
}
#[derive(Clone, Default)]
pub struct TitleContext {
el: Rc<RefCell<Option<web_sys::HtmlTitleElement>>>,
formatter: Rc<RefCell<Option<Formatter>>>,
text: Rc<RefCell<Option<TextProp>>>,
}
#[cfg(feature = "ssr")]
pub fn dehydrate(&self) -> String {
let mut tags = String::new();
impl Debug for TitleContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("TitleContext").finish()
// Title
if let Some(title) = self.title.as_string() {
tags.push_str("<title>");
tags.push_str(&title);
tags.push_str("</title>");
}
// Stylesheets
tags.push_str(&self.stylesheets.as_string());
tags
}
}
@ -65,73 +75,3 @@ where
TextProp(Box::new(s))
}
}
pub struct Formatter(Box<dyn Fn(String) -> String>);
impl<F> From<F> for Formatter
where
F: Fn(String) -> String + 'static,
{
fn from(f: F) -> Formatter {
Formatter(Box::new(f))
}
}
#[cfg(feature = "ssr")]
#[component]
pub fn Title(cx: Scope, formatter: Option<Formatter>, text: Option<TextProp>) {
let meta = use_head(cx);
if let Some(formatter) = formatter {
*meta.title.formatter.borrow_mut() = Some(formatter);
}
if let Some(text) = text {
*meta.title.text.borrow_mut() = Some(text.into());
}
}
#[cfg(not(feature = "ssr"))]
#[component]
pub fn Title(cx: Scope, formatter: Option<Formatter>, text: Option<TextProp>) {
let meta = use_head(cx);
if let Some(formatter) = formatter {
*meta.title.formatter.borrow_mut() = Some(formatter);
}
if let Some(text) = text {
*meta.title.text.borrow_mut() = Some(text.into());
}
let el_ref = meta.title.el.borrow_mut();
let el = if let Some(el) = &*el_ref {
el.clone()
} else {
match document().query_selector("title") {
Ok(Some(title)) => title.unchecked_into(),
_ => {
let el = document().create_element("title").unwrap_throw();
document()
.query_selector("head")
.unwrap_throw()
.unwrap_throw()
.append_child(el.unchecked_ref())
.unwrap_throw();
el.unchecked_into()
}
}
};
create_render_effect(cx, move |_| {
let text = meta
.title
.text
.borrow()
.as_ref()
.map(|f| (f.0)())
.unwrap_or_default();
let text = if let Some(formatter) = &*meta.title.formatter.borrow() {
(formatter.0)(text)
} else {
text
};
el.set_text_content(Some(&text));
});
}

52
meta/src/stylesheet.rs Normal file
View file

@ -0,0 +1,52 @@
use crate::{use_head, MetaContext};
use leptos::*;
use std::{cell::RefCell, collections::HashMap, rc::Rc};
#[derive(Clone, Default, Debug)]
pub struct StylesheetContext {
els: Rc<RefCell<HashMap<String, Option<web_sys::HtmlLinkElement>>>>,
}
impl StylesheetContext {
pub fn as_string(&self) -> String {
self.els
.borrow()
.iter()
.map(|(href, _)| format!(r#"<link rel="stylesheet" href="{href}">"#))
.collect()
}
}
#[cfg(feature = "ssr")]
#[component]
pub fn Stylesheet(cx: Scope, href: String) {
let meta = use_head(cx);
meta.stylesheets.els.borrow_mut().insert(href, None);
}
#[cfg(not(feature = "ssr"))]
#[component]
pub fn Stylesheet(cx: Scope, href: String) {
use leptos::document;
let meta = use_head(cx);
let existing_el = meta.stylesheets.els.borrow();
let existing_el = existing_el.get(&href).clone();
if let Some(Some(_)) = existing_el {
log::warn!("<Stylesheet/> already loaded stylesheet {href}");
} else {
let el = document().create_element("link").unwrap_throw();
el.set_attribute("rel", "stylesheet").unwrap_throw();
el.set_attribute("href", &href).unwrap_throw();
document()
.query_selector("head")
.unwrap_throw()
.unwrap_throw()
.append_child(el.unchecked_ref())
.unwrap_throw();
meta.stylesheets
.els
.borrow_mut()
.insert(href, Some(el.unchecked_into()));
}
}

95
meta/src/title.rs Normal file
View file

@ -0,0 +1,95 @@
use crate::{use_head, MetaContext, TextProp};
use leptos::*;
use std::{cell::RefCell, rc::Rc};
#[derive(Clone, Default)]
pub struct TitleContext {
el: Rc<RefCell<Option<web_sys::HtmlTitleElement>>>,
formatter: Rc<RefCell<Option<Formatter>>>,
text: Rc<RefCell<Option<TextProp>>>,
}
impl TitleContext {
pub fn as_string(&self) -> Option<String> {
let title = self.text.borrow().as_ref().map(|f| (f.0)());
title.map(|title| {
if let Some(formatter) = &*self.formatter.borrow() {
(formatter.0)(title)
} else {
title
}
})
}
}
impl std::fmt::Debug for TitleContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("TitleContext").finish()
}
}
pub struct Formatter(Box<dyn Fn(String) -> String>);
impl<F> From<F> for Formatter
where
F: Fn(String) -> String + 'static,
{
fn from(f: F) -> Formatter {
Formatter(Box::new(f))
}
}
#[cfg(feature = "ssr")]
#[component]
pub fn Title(cx: Scope, formatter: Option<Formatter>, text: Option<TextProp>) {
let meta = use_head(cx);
if let Some(formatter) = formatter {
*meta.title.formatter.borrow_mut() = Some(formatter);
}
if let Some(text) = text {
*meta.title.text.borrow_mut() = Some(text.into());
}
log::debug!("setting title to {:?}", meta.title.as_string());
}
#[cfg(not(feature = "ssr"))]
#[component]
pub fn Title(cx: Scope, formatter: Option<Formatter>, text: Option<TextProp>) {
use crate::use_head;
let meta = use_head(cx);
if let Some(formatter) = formatter {
*meta.title.formatter.borrow_mut() = Some(formatter);
}
if let Some(text) = text {
*meta.title.text.borrow_mut() = Some(text.into());
}
let el = {
let el_ref = meta.title.el.borrow_mut();
let el = if let Some(el) = &*el_ref {
el.clone()
} else {
match document().query_selector("title") {
Ok(Some(title)) => title.unchecked_into(),
_ => {
let el = document().create_element("title").unwrap_throw();
document()
.query_selector("head")
.unwrap_throw()
.unwrap_throw()
.append_child(el.unchecked_ref())
.unwrap_throw();
el.unchecked_into()
}
}
};
el
};
create_render_effect(cx, move |_| {
let text = meta.title.as_string().unwrap_or_default();
el.set_text_content(Some(&text));
});
}