Merge pull request #252 from gbj/additional-meta-tags

Additional meta tags — closes issue #158
This commit is contained in:
Greg Johnston 2023-01-07 07:37:02 -05:00 committed by GitHub
commit e12c2d9769
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 400 additions and 258 deletions

View file

@ -15,7 +15,7 @@ pub fn App(cx: Scope) -> impl IntoView {
view! {
cx,
<>
<Stylesheet id="leptos" href="./target/site/pkg/hackernews.css"/>
<Stylesheet id="leptos" href="/target/site/pkg/hackernews.css"/>
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
<Router>
<Nav />

View file

@ -327,7 +327,8 @@ pub struct Text {
/// to possibly reuse a previous node.
#[cfg(all(target_arch = "wasm32", feature = "web"))]
node: web_sys::Node,
content: Cow<'static, str>,
/// The current contents of the text node.
pub content: Cow<'static, str>,
}
impl fmt::Debug for Text {

View file

@ -10,6 +10,7 @@ description = "Tools to set HTML metadata in the Leptos web framework."
[dependencies]
cfg-if = "1"
leptos = { path = "../leptos", version = "0.1.0-beta", default-features = false }
tracing = "0.1"
typed-builder = "0.11"
[dependencies.web-sys]
@ -18,10 +19,10 @@ features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"]
[features]
default = ["csr"]
csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"]
ssr = ["leptos/ssr"]
stable = ["leptos/stable"]
csr = ["leptos/csr", "leptos/tracing"]
hydrate = ["leptos/hydrate", "leptos/tracing"]
ssr = ["leptos/ssr", "leptos/tracing"]
stable = ["leptos/stable", "leptos/tracing"]
[package.metadata.cargo-all-features]
denylist = ["stable"]

View file

@ -36,14 +36,26 @@
//!
//! ```
use std::{fmt::Debug, rc::Rc};
use cfg_if::cfg_if;
use std::{
cell::{Cell, RefCell},
collections::HashMap,
fmt::Debug,
rc::Rc,
};
use leptos::{leptos_dom::debug_warn, *};
mod link;
mod meta_tags;
mod script;
mod style;
mod stylesheet;
mod title;
pub use link::*;
pub use meta_tags::*;
pub use script::*;
pub use style::*;
pub use stylesheet::*;
pub use title::*;
@ -51,11 +63,87 @@ pub use title::*;
///
/// This should generally by provided somewhere in the root of your application using
/// [provide_meta_context].
#[derive(Debug, Clone, Default)]
#[derive(Clone, Default)]
pub struct MetaContext {
pub(crate) title: TitleContext,
pub(crate) stylesheets: StylesheetContext,
pub(crate) meta_tags: MetaTagsContext,
pub(crate) tags: MetaTagsContext,
}
/// Manages all of the element created by components.
#[derive(Clone, Default)]
pub(crate) struct MetaTagsContext {
next_id: Rc<Cell<MetaTagId>>,
#[allow(clippy::type_complexity)]
els: Rc<RefCell<HashMap<String, (HtmlElement<AnyElement>, Scope, Option<web_sys::Element>)>>>,
}
impl MetaTagsContext {
#[cfg(feature = "ssr")]
pub fn as_string(&self) -> String {
println!(
"\n\nrendering {} elements to strings\n\n",
self.els.borrow().len()
);
self.els
.borrow()
.iter()
.map(|(_, (builder_el, cx, _))| builder_el.clone().into_view(*cx).render_to_string(*cx))
.collect()
}
pub fn register(&self, cx: Scope, id: String, builder_el: HtmlElement<AnyElement>) {
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
use leptos::document;
let element_to_hydrate = document()
.get_element_by_id(&id);
let el = element_to_hydrate.unwrap_or_else({
let builder_el = builder_el.clone();
move || {
let head = document().head().unwrap_throw();
head
.append_child(&builder_el)
.unwrap_throw();
(*builder_el).clone().unchecked_into()
}
});
on_cleanup(cx, {
let el = el.clone();
let els = self.els.clone();
let id = id.clone();
move || {
let head = document().head().unwrap_throw();
_ = head.remove_child(&el);
els.borrow_mut().remove(&id);
}
});
self
.els
.borrow_mut()
.insert(id, (builder_el.into_any(), cx, Some(el)));
} else {
self.els.borrow_mut().insert(id, (builder_el, cx, None));
}
}
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
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
}
}
/// Provides a [MetaContext], if there is not already one provided. This ensures that you can provide it
@ -98,7 +186,7 @@ impl MetaContext {
Default::default()
}
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
#[cfg(feature = "ssr")]
/// Converts the existing metadata tags into HTML that can be injected into the document head.
///
/// This should be called *after* the apps component tree has been rendered into HTML, so that
@ -123,14 +211,15 @@ impl MetaContext {
/// // `app` contains only the body content w/ hydration stuff, not the meta tags
/// assert_eq!(
/// app.into_view(cx).render_to_string(cx),
/// "<main id=\"_0-1\"><leptos-unit leptos id=_0-2c></leptos-unit><leptos-unit leptos id=_0-3c></leptos-unit><p id=\"_0-4\">Some text</p></main>"
/// "<main id=\"_0-1\"><leptos-unit leptos id=_0-2c></leptos-unit><leptos-unit leptos id=_0-4c></leptos-unit><p id=\"_0-5\">Some text</p></main>"
/// );
/// // `MetaContext::dehydrate()` gives you HTML that should be in the `<head>`
/// assert_eq!(use_head(cx).dehydrate(), r#"<title>my title</title><link rel="stylesheet" href="/style.css">"#)
/// assert_eq!(use_head(cx).dehydrate(), r#"<title>my title</title><link id="leptos-link-1" href="/style.css" rel="stylesheet" leptos-hk="_0-3"/>"#)
/// });
/// # }
/// ```
pub fn dehydrate(&self) -> String {
let prev_key = HydrationCtx::peek();
let mut tags = String::new();
// Title
@ -139,12 +228,9 @@ impl MetaContext {
tags.push_str(&title);
tags.push_str("</title>");
}
// Stylesheets
tags.push_str(&self.stylesheets.as_string());
// Meta tags
tags.push_str(&self.meta_tags.as_string());
tags.push_str(&self.tags.as_string());
HydrationCtx::continue_from(prev_key);
tags
}
}

109
meta/src/link.rs Normal file
View file

@ -0,0 +1,109 @@
use crate::use_head;
use leptos::*;
/// Injects an [HTMLLinkElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document
/// head, accepting any of the valid attributes for that tag.
/// ```
/// use leptos::*;
/// use leptos_meta::*;
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
///
/// view! { cx,
/// <main>
/// <Link rel="preload"
/// href="myFont.woff2"
/// as_="font"
/// type_="font/woff2"
/// crossorigin="anonymous"
/// />
/// </main>
/// }
/// }
/// ```
#[component(transparent)]
pub fn Link(
cx: Scope,
/// The [`id`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-id) attribute.
#[prop(optional, into)]
id: Option<String>,
/// The [`as`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-as) attribute.
#[prop(optional, into)]
as_: Option<String>,
/// The [`crossorigin`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-crossorigin) attribute.
#[prop(optional, into)]
crossorigin: Option<String>,
/// The [`disabled`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-disabled) attribute.
#[prop(optional, into)]
disabled: Option<bool>,
/// The [`fetchpriority`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-fetchpriority) attribute.
#[prop(optional, into)]
fetchpriority: Option<String>,
/// The [`href`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-href) attribute.
#[prop(optional, into)]
href: Option<String>,
/// The [`hreflang`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-hreflang) attribute.
#[prop(optional, into)]
hreflang: Option<String>,
/// The [`imagesizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-imagesizes) attribute.
#[prop(optional, into)]
imagesizes: Option<String>,
/// The [`imagesrcset`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-imagesrcset) attribute.
#[prop(optional, into)]
imagesrcset: Option<String>,
/// The [`integrity`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-integrity) attribute.
#[prop(optional, into)]
integrity: Option<String>,
/// The [`media`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-media) attribute.
#[prop(optional, into)]
media: Option<String>,
/// The [`prefetch`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-prefetch) attribute.
#[prop(optional, into)]
prefetch: Option<String>,
/// The [`referrerpolicy`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-referrerpolicy) attribute.
#[prop(optional, into)]
referrerpolicy: Option<String>,
/// The [`rel`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-rel) attribute.
#[prop(optional, into)]
rel: Option<String>,
/// The [`sizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-sizes) attribute.
#[prop(optional, into)]
sizes: Option<String>,
/// The [`title`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-title) attribute.
#[prop(optional, into)]
title: Option<String>,
/// The [`type`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-type) attribute.
#[prop(optional, into)]
type_: Option<String>,
/// The [`blocking`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-blocking) attribute.
#[prop(optional, into)]
blocking: Option<String>,
) -> impl IntoView {
let meta = use_head(cx);
let next_id = meta.tags.get_next_id();
let id = id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0));
let builder_el = leptos::link(cx)
.attr("id", &id)
.attr("as_", as_)
.attr("crossorigin", crossorigin)
.attr("disabled", disabled.unwrap_or(false))
.attr("fetchpriority", fetchpriority)
.attr("href", href)
.attr("hreflang", hreflang)
.attr("imagesizes", imagesizes)
.attr("imagesrcset", imagesrcset)
.attr("integrity", integrity)
.attr("media", media)
.attr("prefetch", prefetch)
.attr("referrerpolicy", referrerpolicy)
.attr("rel", rel)
.attr("sizes", sizes)
.attr("title", title)
.attr("type", type_)
.attr("blocking", blocking);
meta.tags.register(cx, id, builder_el.into_any());
}

View file

@ -1,73 +1,7 @@
use cfg_if::cfg_if;
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)]
els: Rc<RefCell<HashMap<MetaTagId, (Option<MetaTag>, Option<web_sys::HtmlMetaElement>)>>>,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
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
}
}
#[derive(Clone, Debug)]
enum MetaTag {
Charset(TextProp),
HttpEquiv {
http_equiv: TextProp,
content: Option<TextProp>,
},
Name {
name: TextProp,
content: TextProp,
},
}
impl MetaTagsContext {
/// Converts the set of `<meta>` elements into an HTML string that can be injected into the `<head>`.
pub fn as_string(&self) -> String {
self.els
.borrow()
.iter()
.filter_map(|(id, (tag, _))| {
tag.as_ref().map(|tag| {
let id = id.0;
match tag {
MetaTag::Charset(charset) => format!(r#"<meta charset="{}" data-leptos-meta="{id}">"#, charset.get()),
MetaTag::HttpEquiv { http_equiv, content } => {
if let Some(content) = &content {
format!(r#"<meta http-equiv="{}" content="{}" data-leptos-meta="{id}">"#, http_equiv.get(), content.get())
} else {
format!(r#"<meta http-equiv="{}" data-leptos-meta="{id}">"#, http_equiv.get())
}
},
MetaTag::Name { name, content } => format!(r#"<meta name="{}" content="{}" data-leptos-meta="{id}">"#, name.get(), content.get()),
}
})
})
.collect()
}
}
/// Injects an [HTMLMetaElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMetaElement) into the document
/// head to set metadata
///
@ -104,89 +38,15 @@ pub fn Meta(
#[prop(optional, into)]
content: Option<TextProp>,
) -> impl IntoView {
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 },
_ => panic!("<Meta/> tag expects either `charset`, `http_equiv`, or `name` and `content` to be set.")
};
let meta = use_head(cx);
let next_id = meta.tags.get_next_id();
let id = format!("leptos-link-{}", next_id.0);
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
use leptos::{document, JsCast, UnwrapThrowExt, create_effect};
let builder_el = leptos::meta(cx)
.attr("charset", move || charset.as_ref().map(|v| v.get()))
.attr("name", move || name.as_ref().map(|v| v.get()))
.attr("http-equiv", move || http_equiv.as_ref().map(|v| v.get()))
.attr("content", move || content.as_ref().map(|v| v.get()));
let meta = use_head(cx);
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()
};
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
let head = document()
.query_selector("head")
.unwrap_throw()
.unwrap_throw();
head.append_child(&el)
.unwrap_throw();
leptos::on_cleanup(cx, {
let el = el.clone();
move || {
head.remove_child(&el);
}
});
// 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;
meta_tags.els.borrow_mut().insert(meta_tags.get_next_id(), (Some(tag), None));
}
}
meta.tags.register(cx, id, builder_el.into_any());
}

98
meta/src/script.rs Normal file
View file

@ -0,0 +1,98 @@
use crate::use_head;
use leptos::*;
/// Injects an [HTMLScriptElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement) into the document
/// head, accepting any of the valid attributes for that tag.
/// ```
/// use leptos::*;
/// use leptos_meta::*;
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
///
/// view! { cx,
/// <main>
/// <Script>
/// "console.log('Hello, world!');"
/// </Script>
/// </main>
/// }
/// }
/// ```
#[component(transparent)]
pub fn Script(
cx: Scope,
/// An ID for the `<script>` tag.
#[prop(optional, into)]
id: Option<String>,
/// The [`async`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-async) attribute.
#[prop(optional, into)]
async_: Option<String>,
/// The [`crossorigin`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-crossorigin) attribute.
#[prop(optional, into)]
crossorigin: Option<String>,
/// The [`defer`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-defer) attribute.
#[prop(optional, into)]
defer: Option<String>,
/// The [`fetchpriority `](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-fetchpriority ) attribute.
#[prop(optional, into)]
fetchpriority: Option<String>,
/// The [`integrity`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-integrity) attribute.
#[prop(optional, into)]
integrity: Option<String>,
/// The [`nomodule`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-nomodule) attribute.
#[prop(optional, into)]
nomodule: Option<String>,
/// The [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-nonce) attribute.
#[prop(optional, into)]
nonce: Option<String>,
/// The [`referrerpolicy`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-referrerpolicy) attribute.
#[prop(optional, into)]
referrerpolicy: Option<String>,
/// The [`src`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-src) attribute.
#[prop(optional, into)]
src: Option<String>,
/// The [`type`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type) attribute.
#[prop(optional, into)]
type_: Option<String>,
/// The [`blocking`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-blocking) attribute.
#[prop(optional, into)]
blocking: Option<String>,
/// The content of the `<script>` tag.
#[prop(optional)]
children: Option<Box<dyn FnOnce(Scope) -> Fragment>>,
) -> impl IntoView {
let meta = use_head(cx);
let next_id = meta.tags.get_next_id();
let id = id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0));
let builder_el = leptos::script(cx)
.attr("id", &id)
.attr("async", async_)
.attr("crossorigin", crossorigin)
.attr("defer", defer)
.attr("fetchpriority ", fetchpriority)
.attr("integrity", integrity)
.attr("nomodule", nomodule)
.attr("nonce", nonce)
.attr("referrerpolicy", referrerpolicy)
.attr("src", src)
.attr("type", type_)
.attr("blocking", blocking);
let builder_el = if let Some(children) = children {
let frag = children(cx);
let mut script = String::new();
for node in frag.nodes {
match node {
View::Text(text) => script.push_str(&text.content),
_ => leptos::warn!("Only text nodes are supported as children of <Script/>."),
}
}
builder_el.child(script)
} else {
builder_el
};
meta.tags.register(cx, id, builder_el.into_any());
}

70
meta/src/style.rs Normal file
View file

@ -0,0 +1,70 @@
use crate::use_head;
use leptos::*;
/// Injects an [HTMLStyleElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLStyleElement) into the document
/// head, accepting any of the valid attributes for that tag.
/// ```
/// use leptos::*;
/// use leptos_meta::*;
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
///
/// view! { cx,
/// <main>
/// <Style>
/// "body { font-weight: bold; }"
/// </Style>
/// </main>
/// }
/// }
/// ```
#[component(transparent)]
pub fn Style(
cx: Scope,
/// An ID for the `<script>` tag.
#[prop(optional, into)]
id: Option<String>,
/// The [`media`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style#attr-media) attribute.
#[prop(optional, into)]
media: Option<String>,
/// The [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style#attr-nonce) attribute.
#[prop(optional, into)]
nonce: Option<String>,
/// The [`title`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style#attr-title) attribute.
#[prop(optional, into)]
title: Option<String>,
/// The [`blocking`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style#attr-blocking) attribute.
#[prop(optional, into)]
blocking: Option<String>,
/// The content of the `<style>` tag.
#[prop(optional)]
children: Option<Box<dyn FnOnce(Scope) -> Fragment>>,
) -> impl IntoView {
let meta = use_head(cx);
let next_id = meta.tags.get_next_id();
let id = id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0));
let builder_el = leptos::style(cx)
.attr("id", &id)
.attr("media", media)
.attr("nonce", nonce)
.attr("title", title)
.attr("blocking", blocking);
let builder_el = if let Some(children) = children {
let frag = children(cx);
let mut style = String::new();
for node in frag.nodes {
match node {
View::Text(text) => style.push_str(&text.content),
_ => leptos::warn!("Only text nodes are supported as children of <Style/>."),
}
}
builder_el.child(style)
} else {
builder_el
};
meta.tags.register(cx, id, builder_el.into_any());
}

View file

@ -1,51 +1,5 @@
use crate::use_head;
use cfg_if::cfg_if;
use crate::{Link, LinkProps};
use leptos::*;
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)]
// 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 {
/// Converts the set of stylesheets into an HTML string that can be injected into the `<head>`.
pub fn as_string(&self) -> String {
self.els
.borrow()
.iter()
.map(|(StyleSheetData { id, href }, _)| {
format!(r#"<link rel="stylesheet" id="{id}" href="{href}">"#)
})
.collect()
}
}
/// Injects an [HTMLLinkElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document
/// head that loads a stylesheet from the URL given by the `href` property.
@ -75,50 +29,13 @@ 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 element_to_hydrate = document().get_element_by_id(&key.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();
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.remove_child(&el);
els.borrow_mut().remove(&key);
}
});
meta.stylesheets
.els
.borrow_mut()
.insert(key, Some(el.unchecked_into()));
} else {
let meta = use_head(cx);
meta.stylesheets.els.borrow_mut().insert(key, None);
if let Some(id) = id {
view! { cx,
<Link id rel="stylesheet" href/>
}
} else {
view! { cx,
<Link rel="stylesheet" href/>
}
}
}