mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 14:54:16 +00:00
Correctly hydrate stylesheets, and correctly clean up stylesheets and metadata
This commit is contained in:
parent
d5bda04306
commit
e2496e01d0
3 changed files with 148 additions and 141 deletions
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue