Merge in updates to meta package

This commit is contained in:
Greg Johnston 2022-12-12 13:39:30 -05:00
parent 035f929d3b
commit 1804a65857
5 changed files with 210 additions and 10 deletions

View File

@ -14,7 +14,7 @@ typed-builder = "0.11"
[dependencies.web-sys]
version = "0.3"
features = ["HtmlLinkElement", "HtmlTitleElement"]
features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"]
[features]
default = ["csr"]

View File

@ -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<dyn Fn() -> String>);
#[derive(Clone)]
pub struct TextProp(Rc<dyn Fn() -> 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<String> 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))
}
}

187
meta/src/meta_tags.rs Normal file
View File

@ -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 `<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()
}
}
/// 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<TextProp>,
/// 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<TextProp>,
/// 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<TextProp>,
/// 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<TextProp>,
}
/// 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,
/// <main>
/// <Meta charset="utf-8"/>
/// <Meta name="description" content="A Leptos fan site."/>
/// <Meta http_equiv="refresh" content="3;url=https://github.com/gbj/leptos"/>
/// </main>
/// }
/// }
/// ```
#[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!("<Meta/> 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));
}
}
}

View File

@ -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

View File

@ -50,10 +50,10 @@ where
pub struct TitleProps {
/// A function that will be applied to any text value before its set as the title.
#[builder(default, setter(strip_option, into))]
formatter: Option<Formatter>,
// Sets the the current `document.title`.
pub formatter: Option<Formatter>,
/// Sets the the current `document.title`.
#[builder(default, setter(strip_option, into))]
text: Option<TextProp>,
pub text: Option<TextProp>,
}
/// A component to set the documents title by creating an [HTMLTitleElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTitleElement).