Add `<Link/>` and refactor `<Stylesheet/>` to use it

This commit is contained in:
Greg Johnston 2023-01-06 16:05:59 -05:00
parent 319a058e63
commit 1850c28d3a
5 changed files with 214 additions and 98 deletions

View File

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

View File

@ -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("</title>");
}
// 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
}
}

194
meta/src/link.rs Normal file
View File

@ -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<RefCell<HashMap<String, (HtmlElement<Link>, Scope, Option<web_sys::HtmlLinkElement>)>>>,
next_id: Rc<Cell<LinkId>>,
}
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 `<head>`.
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,
/// <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 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::<web_sys::HtmlLinkElement>());
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));
}
}
}

View File

@ -177,7 +177,7 @@ pub fn Meta(
leptos::on_cleanup(cx, {
let el = el.clone();
move || {
head.remove_child(&el);
_ = head.remove_child(&el);
}
});

View File

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