From 1850c28d3adc49f3fb94abac8808a7a9190e6742 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Fri, 6 Jan 2023 16:05:59 -0500 Subject: [PATCH] Add `` and refactor `` to use it --- meta/Cargo.toml | 9 +- meta/src/lib.rs | 8 +- meta/src/link.rs | 194 +++++++++++++++++++++++++++++++++++++++++ meta/src/meta_tags.rs | 2 +- meta/src/stylesheet.rs | 99 ++------------------- 5 files changed, 214 insertions(+), 98 deletions(-) create mode 100644 meta/src/link.rs diff --git a/meta/Cargo.toml b/meta/Cargo.toml index c1c347262..e8323d755 100644 --- a/meta/Cargo.toml +++ b/meta/Cargo.toml @@ -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"] diff --git a/meta/src/lib.rs b/meta/src/lib.rs index 90043e4e7..8abb9e5b3 100644 --- a/meta/src/lib.rs +++ b/meta/src/lib.rs @@ -40,9 +40,11 @@ use std::{fmt::Debug, rc::Rc}; use leptos::{leptos_dom::debug_warn, *}; +mod link; mod meta_tags; mod stylesheet; mod title; +pub use link::*; pub use meta_tags::*; pub use stylesheet::*; pub use title::*; @@ -54,8 +56,8 @@ pub use title::*; #[derive(Debug, Clone, Default)] pub struct MetaContext { pub(crate) title: TitleContext, - pub(crate) stylesheets: StylesheetContext, pub(crate) meta_tags: MetaTagsContext, + pub(crate) links: LinkContext, } /// Provides a [MetaContext], if there is not already one provided. This ensures that you can provide it @@ -131,6 +133,7 @@ impl MetaContext { /// # } /// ``` pub fn dehydrate(&self) -> String { + let prev_key = HydrationCtx::peek(); let mut tags = String::new(); // Title @@ -140,11 +143,12 @@ impl MetaContext { tags.push_str(""); } // Stylesheets - tags.push_str(&self.stylesheets.as_string()); + tags.push_str(&self.links.as_string()); // Meta tags tags.push_str(&self.meta_tags.as_string()); + HydrationCtx::continue_from(prev_key); tags } } diff --git a/meta/src/link.rs b/meta/src/link.rs new file mode 100644 index 000000000..1a2ccb3c8 --- /dev/null +++ b/meta/src/link.rs @@ -0,0 +1,194 @@ +use crate::use_head; +use cfg_if::cfg_if; +use leptos::*; +use std::{ + cell::{Cell, RefCell}, + collections::HashMap, + rc::Rc, +}; + +/// Manages all of the Links set by [Link] components. +#[derive(Clone, Default)] +pub struct LinkContext { + #[allow(clippy::type_complexity)] + els: Rc, Scope, Option)>>>, + next_id: Rc>, +} + +impl std::fmt::Debug for LinkContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LinkContext").finish() + } +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] +struct LinkId(usize); + +impl LinkContext { + fn get_next_id(&self) -> LinkId { + let current_id = self.next_id.get(); + let next_id = LinkId(current_id.0 + 1); + self.next_id.set(next_id); + next_id + } +} + +#[cfg(feature = "ssr")] +impl LinkContext { + /// Converts the set of Links into an HTML string that can be injected into the ``. + pub fn as_string(&self) -> String { + self.els + .borrow() + .iter() + .map(|(_, (builder_el, cx, _))| builder_el.clone().into_view(*cx).render_to_string(*cx)) + .collect() + } +} + +/// Injects an [HTMLLinkElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document +/// head. +/// ``` +/// use leptos::*; +/// use leptos_meta::*; +/// +/// #[component] +/// fn MyApp(cx: Scope) -> impl IntoView { +/// provide_meta_context(cx); +/// +/// view! { cx, +///
+/// +///
+/// } +/// } +/// ``` +#[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, + /// The [`as`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-as) attribute. + #[prop(optional, into)] + as_: Option, + /// The [`crossorigin`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-crossorigin) attribute. + #[prop(optional, into)] + crossorigin: Option, + /// The [`disabled`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-disabled) attribute. + #[prop(optional, into)] + disabled: Option, + /// The [`fetchpriority`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-fetchpriority) attribute. + #[prop(optional, into)] + fetchpriority: Option, + /// The [`href`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-href) attribute. + #[prop(optional, into)] + href: Option, + /// The [`hreflang`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-hreflang) attribute. + #[prop(optional, into)] + hreflang: Option, + /// The [`imagesizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-imagesizes) attribute. + #[prop(optional, into)] + imagesizes: Option, + /// The [`imagesrcset`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-imagesrcset) attribute. + #[prop(optional, into)] + imagesrcset: Option, + /// The [`integrity`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-integrity) attribute. + #[prop(optional, into)] + integrity: Option, + /// The [`media`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-media) attribute. + #[prop(optional, into)] + media: Option, + /// The [`prefetch`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-prefetch) attribute. + #[prop(optional, into)] + prefetch: Option, + /// The [`referrerpolicy`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-referrerpolicy) attribute. + #[prop(optional, into)] + referrerpolicy: Option, + /// The [`rel`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-rel) attribute. + #[prop(optional, into)] + rel: Option, + /// The [`sizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-sizes) attribute. + #[prop(optional, into)] + sizes: Option, + /// The [`title`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-title) attribute. + #[prop(optional, into)] + title: Option, + /// The [`type`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-type) attribute. + #[prop(optional, into)] + type_: Option, + /// The [`blocking`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-blocking) attribute. + #[prop(optional, into)] + blocking: Option, +) -> impl IntoView { + let meta = use_head(cx); + let links = &meta.links; + let next_id = links.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); + + cfg_if! { + if #[cfg(any(feature = "csr", feature = "hydrate"))] { + use leptos::document; + + let element_to_hydrate = document() + .get_element_by_id(&id) + .map(|el| el.unchecked_into::()); + + 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() + } + }); + + on_cleanup(cx, { + let el = el.clone(); + let els = meta.links.els.clone(); + let id = id.clone(); + move || { + let head = document().head().unwrap_throw(); + _ = head.remove_child(&el); + els.borrow_mut().remove(&id); + } + }); + + meta.links + .els + .borrow_mut() + .insert(id, (builder_el, cx, Some(el))); + + } else { + let meta = use_head(cx); + meta.links.els.borrow_mut().insert(id, (builder_el, cx, None)); + } + } +} diff --git a/meta/src/meta_tags.rs b/meta/src/meta_tags.rs index fff3868d3..4e280a916 100644 --- a/meta/src/meta_tags.rs +++ b/meta/src/meta_tags.rs @@ -177,7 +177,7 @@ pub fn Meta( leptos::on_cleanup(cx, { let el = el.clone(); move || { - head.remove_child(&el); + _ = head.remove_child(&el); } }); diff --git a/meta/src/stylesheet.rs b/meta/src/stylesheet.rs index 3d96de2c9..907198c05 100644 --- a/meta/src/stylesheet.rs +++ b/meta/src/stylesheet.rs @@ -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>>>, - next_id: Rc>, -} - -#[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 ``. - pub fn as_string(&self) -> String { - self.els - .borrow() - .iter() - .map(|(StyleSheetData { id, href }, _)| { - format!(r#""#) - }) - .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, ) -> 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, + + } + } else { + view! { cx, + } } }