Merge in updates to meta package
This commit is contained in:
parent
035f929d3b
commit
1804a65857
|
@ -14,7 +14,7 @@ typed-builder = "0.11"
|
|||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = ["HtmlLinkElement", "HtmlTitleElement"]
|
||||
features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"]
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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<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 document’s title by creating an [HTMLTitleElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTitleElement).
|
||||
|
|
Loading…
Reference in New Issue