mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 14:54:16 +00:00
Merge pull request #252 from gbj/additional-meta-tags
Additional meta tags — closes issue #158
This commit is contained in:
commit
e12c2d9769
9 changed files with 400 additions and 258 deletions
|
@ -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 />
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"]
|
||||
|
|
110
meta/src/lib.rs
110
meta/src/lib.rs
|
@ -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 app’s 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
109
meta/src/link.rs
Normal 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());
|
||||
}
|
|
@ -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
98
meta/src/script.rs
Normal 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
70
meta/src/style.rs
Normal 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());
|
||||
}
|
|
@ -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/>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue