1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
use crate::{use_head, TextProp};
use cfg_if::cfg_if;
use leptos::*;
use std::{cell::RefCell, rc::Rc};
#[cfg(any(feature = "csr", feature = "hydrate"))]
use wasm_bindgen::{JsCast, UnwrapThrowExt};
/// Contains the current state of the document's `<title>`.
#[derive(Clone, Default)]
pub struct TitleContext {
#[cfg(any(feature = "csr", feature = "hydrate"))]
el: Rc<RefCell<Option<web_sys::HtmlTitleElement>>>,
formatter: Rc<RefCell<Option<Formatter>>>,
text: Rc<RefCell<Option<TextProp>>>,
}
impl TitleContext {
/// Converts the title into a string that can be used as the text content of a `<title>` tag.
pub fn as_string(&self) -> Option<Oco<'static, str>> {
let title = self.text.borrow().as_ref().map(TextProp::get);
title.map(|title| {
if let Some(formatter) = &*self.formatter.borrow() {
(formatter.0)(title.into_owned()).into()
} else {
title
}
})
}
}
impl std::fmt::Debug for TitleContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("TitleContext").finish()
}
}
/// A function that is applied to the text value before setting `document.title`.
#[repr(transparent)]
pub struct Formatter(Box<dyn Fn(String) -> String>);
impl<F> From<F> for Formatter
where
F: Fn(String) -> String + 'static,
{
#[inline(always)]
fn from(f: F) -> Formatter {
Formatter(Box::new(f))
}
}
/// A component to set the document’s title by creating an [HTMLTitleElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTitleElement).
///
/// The `title` and `formatter` can be set independently of one another. For example, you can create a root-level
/// `<Title formatter=.../>` that will wrap each of the text values of `<Title/>` components created lower in the tree.
///
/// ```
/// use leptos::*;
/// use leptos_meta::*;
///
/// #[component]
/// fn MyApp() -> impl IntoView {
/// provide_meta_context();
/// let formatter = |text| format!("{text} — Leptos Online");
///
/// view! {
/// <main>
/// <Title formatter/>
/// // ... routing logic here
/// </main>
/// }
/// }
///
/// #[component]
/// fn PageA() -> impl IntoView {
/// view! {
/// <main>
/// <Title text="Page A"/> // sets title to "Page A — Leptos Online"
/// </main>
/// }
/// }
///
/// #[component]
/// fn PageB() -> impl IntoView {
/// view! {
/// <main>
/// <Title text="Page B"/> // sets title to "Page B — Leptos Online"
/// </main>
/// }
/// }
/// ```
#[component(transparent)]
pub fn Title(
/// A function that will be applied to any text value before it’s set as the title.
#[prop(optional, into)]
formatter: Option<Formatter>,
/// Sets the the current `document.title`.
#[prop(optional, into)]
text: Option<TextProp>,
) -> impl IntoView {
let meta = use_head();
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
if let Some(formatter) = formatter {
*meta.title.formatter.borrow_mut() = Some(formatter);
}
if let Some(text) = text {
*meta.title.text.borrow_mut() = Some(text);
}
let el = {
let mut el_ref = meta.title.el.borrow_mut();
let el = if let Some(el) = &*el_ref {
el.clone()
} else {
match document().query_selector("title") {
Ok(Some(title)) => title.unchecked_into(),
_ => {
let el_ref = meta.title.el.clone();
let el = document().create_element("title").unwrap_throw();
let head = document().head().unwrap_throw();
head.append_child(el.unchecked_ref())
.unwrap_throw();
on_cleanup({
let el = el.clone();
move || {
_ = head.remove_child(&el);
*el_ref.borrow_mut() = None;
}
});
el.unchecked_into()
}
}
};
*el_ref = Some(el.clone().unchecked_into());
el
};
create_render_effect(move |_| {
let text = meta.title.as_string().unwrap_or_default();
el.set_text_content(Some(&text));
});
} else {
if let Some(formatter) = formatter {
*meta.title.formatter.borrow_mut() = Some(formatter);
}
if let Some(text) = text {
*meta.title.text.borrow_mut() = Some(text);
}
}
}
}