diff --git a/meta/Cargo.toml b/meta/Cargo.toml index 248b8b8d7..f167b76a1 100644 --- a/meta/Cargo.toml +++ b/meta/Cargo.toml @@ -14,7 +14,7 @@ typed-builder = "0.11" [dependencies.web-sys] version = "0.3" -features = ["HtmlLinkElement", "HtmlTitleElement"] +features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"] [features] default = ["csr"] diff --git a/meta/src/lib.rs b/meta/src/lib.rs index c77a377a5..3fc2e8c86 100644 --- a/meta/src/lib.rs +++ b/meta/src/lib.rs @@ -36,12 +36,14 @@ //! //! ``` -use std::fmt::Debug; +use std::{fmt::Debug, rc::Rc}; use leptos::{leptos_dom::debug_warn, *}; +mod meta_tags; mod stylesheet; mod title; +pub use meta_tags::*; pub use stylesheet::*; pub use title::*; @@ -53,6 +55,7 @@ pub use title::*; pub struct MetaContext { pub(crate) title: TitleContext, pub(crate) stylesheets: StylesheetContext, + pub(crate) meta_tags: MetaTagsContext } /// Returns the current [MetaContext]. @@ -123,13 +126,23 @@ impl MetaContext { // Stylesheets tags.push_str(&self.stylesheets.as_string()); + // Meta tags + tags.push_str(&self.meta_tags.as_string()); + tags } } /// Describes a value that is either a static or a reactive string, i.e., /// a [String], a [&str], or a reactive `Fn() -> String`. -pub struct TextProp(Box String>); +#[derive(Clone)] +pub struct TextProp(Rc String>); + +impl TextProp { + fn get(&self) -> String { + (self.0)() + } +} impl Debug for TextProp { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -139,14 +152,14 @@ impl Debug for TextProp { impl From for TextProp { fn from(s: String) -> Self { - TextProp(Box::new(move || s.clone())) + TextProp(Rc::new(move || s.clone())) } } impl From<&str> for TextProp { fn from(s: &str) -> Self { let s = s.to_string(); - TextProp(Box::new(move || s.clone())) + TextProp(Rc::new(move || s.clone())) } } @@ -155,6 +168,6 @@ where F: Fn() -> String + 'static, { fn from(s: F) -> Self { - TextProp(Box::new(s)) + TextProp(Rc::new(s)) } } diff --git a/meta/src/meta_tags.rs b/meta/src/meta_tags.rs new file mode 100644 index 000000000..868ec3b21 --- /dev/null +++ b/meta/src/meta_tags.rs @@ -0,0 +1,187 @@ +use cfg_if::cfg_if; +use leptos::Scope; +use std::{rc::Rc, cell::{RefCell, Cell}, collections::HashMap}; +use typed_builder::TypedBuilder; + +use crate::{use_head, TextProp}; + +/// Manages all of the `` elements set by [Meta] components. +#[derive(Clone, Default, Debug)] +pub struct MetaTagsContext { + next_id: Cell, + #[allow(clippy::type_complexity)] + els: Rc, Option)>>>, +} + +#[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 + }, + Name { + name: TextProp, + content: TextProp + } +} + +impl MetaTagsContext { + /// Converts the set of `` elements into an HTML string that can be injected into the ``. + 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#""#, charset.get()), + MetaTag::HttpEquiv { http_equiv, content } => { + if let Some(content) = &content { + format!(r#""#, http_equiv.get(), content.get()) + } else { + format!(r#""#, http_equiv.get()) + } + }, + MetaTag::Name { name, content } => format!(r#""#, name.get(), content.get()), + } + }) + }) + .collect() + } +} + +/// Properties for the [Meta] component. +#[derive(TypedBuilder)] +pub struct MetaProps { + /// The [`charset`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-charset) attribute. + #[builder(default, setter(strip_option, into))] + pub charset: Option, + /// The [`name`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-name) attribute. + #[builder(default, setter(strip_option, into))] + pub name: Option, + /// The [`http-equiv`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-http-equiv) attribute. + #[builder(default, setter(strip_option, into))] + pub http_equiv: Option, + /// The [`content`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-content) attribute. + #[builder(default, setter(strip_option, into))] + pub content: Option, +} + +/// Injects an [HTMLMetaElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMetaElement) into the document +/// head to set metadata +/// +/// ``` +/// use leptos::*; +/// use leptos_meta::*; +/// +/// #[component] +/// fn MyApp(cx: Scope) -> Element { +/// provide_context(cx, MetaContext::new()); +/// +/// view! { cx, +///
+/// +/// +/// +///
+/// } +/// } +/// ``` +#[allow(non_snake_case)] +pub fn Meta(cx: Scope, props: MetaProps) { + let MetaProps { charset, name, http_equiv, content } = props; + + 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!(" tag expects either `charset`, `http_equiv`, or `name` and `content` to be set.") + }; + + cfg_if! { + if #[cfg(any(feature = "csr", feature = "hydrate"))] { + 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 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 + document() + .query_selector("head") + .unwrap_throw() + .unwrap_throw() + .append_child(&el) + .unwrap_throw(); + + // 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)); + } + } +} diff --git a/meta/src/stylesheet.rs b/meta/src/stylesheet.rs index c050b30fc..00d6aa9bf 100644 --- a/meta/src/stylesheet.rs +++ b/meta/src/stylesheet.rs @@ -26,7 +26,7 @@ impl StylesheetContext { pub struct StylesheetProps { /// The URL at which the stylesheet can be located. #[builder(setter(into))] - href: String, + pub href: String, } /// Injects an [HTMLLinkElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document diff --git a/meta/src/title.rs b/meta/src/title.rs index d21d094c8..685acf714 100644 --- a/meta/src/title.rs +++ b/meta/src/title.rs @@ -50,10 +50,10 @@ where pub struct TitleProps { /// A function that will be applied to any text value before it’s set as the title. #[builder(default, setter(strip_option, into))] - formatter: Option, - // Sets the the current `document.title`. + pub formatter: Option, + /// Sets the the current `document.title`. #[builder(default, setter(strip_option, into))] - text: Option, + pub text: Option, } /// A component to set the document’s title by creating an [HTMLTitleElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTitleElement).