Add `<Link/>` and refactor `<Stylesheet/>` to use it
This commit is contained in:
parent
319a058e63
commit
1850c28d3a
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -177,7 +177,7 @@ pub fn Meta(
|
|||
leptos::on_cleanup(cx, {
|
||||
let el = el.clone();
|
||||
move || {
|
||||
head.remove_child(&el);
|
||||
_ = head.remove_child(&el);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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/>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue