Correctly hydrate stylesheets, and correctly clean up stylesheets and metadata

This commit is contained in:
Greg Johnston 2023-01-04 22:39:54 -05:00
parent d5bda04306
commit e2496e01d0
3 changed files with 148 additions and 141 deletions

View file

@ -1,14 +1,18 @@
use cfg_if::cfg_if;
use leptos::{Scope, component, IntoView, on_cleanup};
use std::{rc::Rc, cell::{RefCell, Cell}, collections::HashMap};
use leptos::{component, IntoView, Scope};
use std::{
cell::{Cell, RefCell},
collections::HashMap,
rc::Rc,
};
use crate::{use_head, TextProp};
/// Manages all of the `<meta>` elements set by [Meta] components.
#[derive(Clone, Default, Debug)]
pub struct MetaTagsContext {
next_id: Cell<MetaTagId>,
#[allow(clippy::type_complexity)]
next_id: Cell<MetaTagId>,
#[allow(clippy::type_complexity)]
els: Rc<RefCell<HashMap<MetaTagId, (Option<MetaTag>, Option<web_sys::HtmlMetaElement>)>>>,
}
@ -16,25 +20,25 @@ pub struct MetaTagsContext {
struct MetaTagId(usize);
impl MetaTagsContext {
fn get_next_id(&self) -> MetaTagId {
let current_id = self.next_id.get();
let next_id = MetaTagId(current_id.0 + 1);
self.next_id.set(next_id);
next_id
}
fn get_next_id(&self) -> MetaTagId {
let current_id = self.next_id.get();
let next_id = MetaTagId(current_id.0 + 1);
self.next_id.set(next_id);
next_id
}
}
#[derive(Clone, Debug)]
enum MetaTag {
Charset(TextProp),
HttpEquiv {
http_equiv: TextProp,
content: Option<TextProp>
},
Name {
name: TextProp,
content: TextProp
}
Charset(TextProp),
HttpEquiv {
http_equiv: TextProp,
content: Option<TextProp>,
},
Name {
name: TextProp,
content: TextProp,
},
}
impl MetaTagsContext {
@ -86,22 +90,21 @@ impl MetaTagsContext {
/// ```
#[component(transparent)]
pub fn Meta(
cx: Scope,
cx: Scope,
/// The [`charset`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-charset) attribute.
#[prop(optional, into)]
charset: Option<TextProp>,
/// The [`name`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-name) attribute.
#[prop(optional, into)]
name: Option<TextProp>,
/// The [`http-equiv`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-http-equiv) attribute.
#[prop(optional, into)]
http_equiv: Option<TextProp>,
/// The [`content`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-content) attribute.
#[prop(optional, into)]
content: Option<TextProp>
/// The [`name`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-name) attribute.
#[prop(optional, into)]
name: Option<TextProp>,
/// The [`http-equiv`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-http-equiv) attribute.
#[prop(optional, into)]
http_equiv: Option<TextProp>,
/// The [`content`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-content) attribute.
#[prop(optional, into)]
content: Option<TextProp>,
) -> impl IntoView {
let tag = match (charset, name, http_equiv, content) {
let tag = match (charset, name, http_equiv, content) {
(Some(charset), _, _, _) => MetaTag::Charset(charset),
(_, _, Some(http_equiv), content) => MetaTag::HttpEquiv { http_equiv, content },
(_, Some(name), _, Some(content)) => MetaTag::Name { name, content },
@ -113,57 +116,57 @@ pub fn Meta(
use leptos::{document, JsCast, UnwrapThrowExt, create_effect};
let meta = use_head(cx);
let meta_tags = meta.meta_tags;
let id = meta_tags.get_next_id();
let meta_tags = meta.meta_tags;
let id = meta_tags.get_next_id();
let el = if let Ok(Some(el)) = document().query_selector(&format!("[data-leptos-meta='{}']", id.0)) {
el
} else {
document().create_element("meta").unwrap_throw()
};
let el = if let Ok(Some(el)) = document().query_selector(&format!("[data-leptos-meta='{}']", id.0)) {
el
} else {
document().create_element("meta").unwrap_throw()
};
match tag {
MetaTag::Charset(charset) => {
create_effect(cx, {
let el = el.clone();
move |_| {
_ = el.set_attribute("charset", &charset.get());
}
})
},
MetaTag::HttpEquiv { http_equiv, content } => {
create_effect(cx, {
let el = el.clone();
move |_| {
_ = el.set_attribute("http-equiv", &http_equiv.get());
}
});
if let Some(content) = content {
create_effect(cx, {
let el = el.clone();
move |_| {
_ = el.set_attribute("content", &content.get());
}
});
}
},
MetaTag::Name { name, content } => {
create_effect(cx, {
let el = el.clone();
move |_| {
_ = el.set_attribute("name", &name.get());
}
});
create_effect(cx, {
let el = el.clone();
move |_| {
_ = el.set_attribute("content", &content.get());
}
});
},
}
match tag {
MetaTag::Charset(charset) => {
create_effect(cx, {
let el = el.clone();
move |_| {
_ = el.set_attribute("charset", &charset.get());
}
})
},
MetaTag::HttpEquiv { http_equiv, content } => {
create_effect(cx, {
let el = el.clone();
move |_| {
_ = el.set_attribute("http-equiv", &http_equiv.get());
}
});
if let Some(content) = content {
create_effect(cx, {
let el = el.clone();
move |_| {
_ = el.set_attribute("content", &content.get());
}
});
}
},
MetaTag::Name { name, content } => {
create_effect(cx, {
let el = el.clone();
move |_| {
_ = el.set_attribute("name", &name.get());
}
});
create_effect(cx, {
let el = el.clone();
move |_| {
_ = el.set_attribute("content", &content.get());
}
});
},
}
// add to head
// add to head
let head = document()
.query_selector("head")
.unwrap_throw()
@ -178,11 +181,11 @@ pub fn Meta(
}
});
// add to meta tags
meta_tags.els.borrow_mut().insert(id, (None, Some(el.unchecked_into())));
// add to meta tags
meta_tags.els.borrow_mut().insert(id, (None, Some(el.unchecked_into())));
} else {
let meta = use_head(cx);
let meta_tags = meta.meta_tags;
let meta_tags = meta.meta_tags;
meta_tags.els.borrow_mut().insert(meta_tags.get_next_id(), (Some(tag), None));
}
}

View file

@ -1,13 +1,37 @@
use crate::use_head;
use cfg_if::cfg_if;
use leptos::*;
use std::{cell::RefCell, collections::HashMap, rc::Rc};
use std::{
cell::{Cell, RefCell},
collections::HashMap,
rc::Rc,
};
/// Manages all of the stylesheets set by [Stylesheet] components.
#[derive(Clone, Default, Debug)]
pub struct StylesheetContext {
#[allow(clippy::type_complexity)]
els: Rc<RefCell<HashMap<(Option<String>, String), Option<web_sys::HtmlLinkElement>>>>,
// key is (id, href)
els: Rc<RefCell<HashMap<StyleSheetData, Option<web_sys::HtmlLinkElement>>>>,
next_id: Rc<Cell<StylesheetId>>,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
struct StylesheetId(usize);
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct StyleSheetData {
id: String,
href: String,
}
impl StylesheetContext {
fn get_next_id(&self) -> StylesheetId {
let current_id = self.next_id.get();
let next_id = StylesheetId(current_id.0 + 1);
self.next_id.set(next_id);
next_id
}
}
impl StylesheetContext {
@ -16,12 +40,8 @@ impl StylesheetContext {
self.els
.borrow()
.iter()
.map(|((id, href), _)| {
if let Some(id) = id {
format!(r#"<link rel="stylesheet" id="{id}" href="{href}">"#)
} else {
format!(r#"<link rel="stylesheet" href="{href}">"#)
}
.map(|(StyleSheetData { id, href }, _)| {
format!(r#"<link rel="stylesheet" id="{id}" href="{href}">"#)
})
.collect()
}
@ -55,61 +75,50 @@ pub fn Stylesheet(
#[prop(optional, into)]
id: Option<String>,
) -> impl IntoView {
let meta = use_head(cx);
let stylesheets = &meta.stylesheets;
let next_id = stylesheets.get_next_id();
let id = id.unwrap_or_else(|| format!("leptos-style-{}", next_id.0));
let key = StyleSheetData { id, href };
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
use leptos::document;
let meta = use_head(cx);
let element_to_hydrate = document().get_element_by_id(&key.id);
let existing_el = {
let els = meta.stylesheets.els.borrow();
let key = (id.clone(), href.clone());
els.get(&key).cloned()
};
if let Some(Some(_)) = existing_el {
leptos::leptos_dom::debug_warn!("<Stylesheet/> already loaded stylesheet {href}");
} else {
let element_to_hydrate = id.as_ref()
.and_then(|id| {
document().get_element_by_id(&id)
});
let el = element_to_hydrate.unwrap_or_else(|| {
let el = document().create_element("link").unwrap_throw();
el.set_attribute("rel", "stylesheet").unwrap_throw();
el.set_attribute("id", &key.id).unwrap_throw();
el.set_attribute("href", &key.href).unwrap_throw();
let head = document().head().unwrap_throw();
head
.append_child(el.unchecked_ref())
.unwrap_throw();
let el = element_to_hydrate.unwrap_or_else(|| {
let el = document().create_element("link").unwrap_throw();
el.set_attribute("rel", "stylesheet").unwrap_throw();
if let Some(id_val) = &id{
el.set_attribute("id", id_val).unwrap_throw();
}
el.set_attribute("href", &href).unwrap_throw();
el
});
on_cleanup(cx, {
let el = el.clone();
let els = meta.stylesheets.els.clone();
let key = key.clone();
move || {
let head = document().head().unwrap_throw();
head
.append_child(el.unchecked_ref())
.unwrap_throw();
_ = head.remove_child(&el);
els.borrow_mut().remove(&key);
}
});
el
});
meta.stylesheets
.els
.borrow_mut()
.insert(key, Some(el.unchecked_into()));
on_cleanup(cx, {
let el = el.clone();
let els = meta.stylesheets.els.clone();
let href = href.clone();
let id = id.clone();
move || {
leptos::log!("removing stylesheet");
let head = document().head().unwrap_throw();
head.remove_child(&el);
els.borrow_mut().remove(&(id, href));
}
});
meta.stylesheets
.els
.borrow_mut()
.insert((id, href), Some(el.unchecked_into()));
}
} else {
let meta = use_head(cx);
meta.stylesheets.els.borrow_mut().insert((id,href), None);
meta.stylesheets.els.borrow_mut().insert(key, None);
}
}
}

View file

@ -96,11 +96,6 @@ pub fn Title(
) -> impl IntoView {
let meta = use_head(cx);
let prev = meta.title.clone();
on_cleanup(cx, move || {
leptos::log!("cleaning up <Title/>");
});
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
if let Some(formatter) = formatter {
@ -117,7 +112,7 @@ pub fn Title(
on_cleanup(cx, {
let el = el.clone();
move || {
el.set_text(&prev_text);
_ = el.set_text(&prev_text);
}
});
@ -134,7 +129,7 @@ pub fn Title(
on_cleanup(cx, {
let el = el.clone();
move || {
head.remove_child(&el);
_ = head.remove_child(&el);
}
});