1use crate::{use_head, MetaContext, ServerMetaContext};
2use leptos::{
3 attr::Attribute,
4 component,
5 oco::Oco,
6 reactive::{
7 effect::RenderEffect,
8 owner::{use_context, Owner},
9 },
10 tachys::{
11 dom::document,
12 hydration::Cursor,
13 view::{
14 add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
15 RenderHtml,
16 },
17 },
18 text_prop::TextProp,
19 IntoView,
20};
21use or_poisoned::OrPoisoned;
22use send_wrapper::SendWrapper;
23use std::sync::{Arc, RwLock};
24use wasm_bindgen::{JsCast, UnwrapThrowExt};
25use web_sys::HtmlTitleElement;
26
27#[derive(Clone, Default)]
29pub struct TitleContext {
30 el: Arc<RwLock<Option<SendWrapper<HtmlTitleElement>>>>,
31 formatter: Arc<RwLock<Option<Formatter>>>,
32 text: Arc<RwLock<Option<TextProp>>>,
33}
34
35impl TitleContext {
36 pub fn as_string(&self) -> Option<Oco<'static, str>> {
38 let title = self.text.read().or_poisoned().as_ref().map(TextProp::get);
39 title.map(|title| {
40 if let Some(formatter) = &*self.formatter.read().or_poisoned() {
41 (formatter.0)(title.into_owned()).into()
42 } else {
43 title
44 }
45 })
46 }
47}
48
49impl core::fmt::Debug for TitleContext {
50 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
51 f.debug_tuple("TitleContext").finish()
52 }
53}
54
55#[repr(transparent)]
57pub struct Formatter(Box<dyn Fn(String) -> String + Send + Sync>);
58
59impl<F> From<F> for Formatter
60where
61 F: Fn(String) -> String + Send + Sync + 'static,
62{
63 #[inline(always)]
64 fn from(f: F) -> Formatter {
65 Formatter(Box::new(f))
66 }
67}
68
69#[component]
110pub fn Title(
111 #[prop(optional, into)]
113 mut formatter: Option<Formatter>,
114 #[prop(optional, into)]
116 mut text: Option<TextProp>,
117) -> impl IntoView {
118 let meta = use_head();
119 let server_ctx = use_context::<ServerMetaContext>();
120 if let Some(cx) = server_ctx {
121 if let Some(formatter) = formatter.take() {
125 *cx.title.formatter.write().or_poisoned() = Some(formatter);
126 }
127 if let Some(text) = text.take() {
128 *cx.title.text.write().or_poisoned() = Some(text);
129 }
130 };
131
132 TitleView {
133 meta,
134 formatter,
135 text,
136 }
137}
138
139struct TitleView {
140 meta: MetaContext,
141 formatter: Option<Formatter>,
142 text: Option<TextProp>,
143}
144
145impl TitleView {
146 fn el(&self) -> HtmlTitleElement {
147 let mut el_ref = self.meta.title.el.write().or_poisoned();
148 let el = if let Some(el) = &*el_ref {
149 el.clone()
150 } else {
151 match document().query_selector("title") {
152 Ok(Some(title)) => SendWrapper::new(title.unchecked_into()),
153 _ => {
154 let el_ref = self.meta.title.el.clone();
155 let el = SendWrapper::new(
156 document()
157 .create_element("title")
158 .unwrap_throw()
159 .unchecked_into::<HtmlTitleElement>(),
160 );
161 let head =
162 SendWrapper::new(document().head().unwrap_throw());
163 head.append_child(el.unchecked_ref()).unwrap_throw();
164
165 Owner::on_cleanup({
166 let el = el.clone();
167 move || {
168 _ = head.remove_child(&el);
169 *el_ref.write().or_poisoned() = None;
170 }
171 });
172
173 el
174 }
175 }
176 };
177 *el_ref = Some(el.clone());
178
179 el.take()
180 }
181}
182
183struct TitleViewState {
184 #[allow(dead_code)]
186 effect: RenderEffect<Oco<'static, str>>,
187}
188
189impl Render for TitleView {
190 type State = TitleViewState;
191
192 fn build(mut self) -> Self::State {
193 let el = self.el();
194 let meta = self.meta;
195 if let Some(formatter) = self.formatter.take() {
196 *meta.title.formatter.write().or_poisoned() = Some(formatter);
197 }
198 if let Some(text) = self.text.take() {
199 *meta.title.text.write().or_poisoned() = Some(text);
200 }
201 let effect = RenderEffect::new({
202 let el = el.clone();
203 move |prev| {
204 let text = meta.title.as_string().unwrap_or_default();
205
206 if prev.as_ref() != Some(&text) {
207 el.set_text_content(Some(&text));
208 }
209
210 text
211 }
212 });
213 TitleViewState { effect }
214 }
215
216 fn rebuild(self, state: &mut Self::State) {
217 *state = self.build();
218 }
219}
220
221impl AddAnyAttr for TitleView {
222 type Output<SomeNewAttr: Attribute> = TitleView;
223
224 fn add_any_attr<NewAttr: Attribute>(
225 self,
226 _attr: NewAttr,
227 ) -> Self::Output<NewAttr>
228 where
229 Self::Output<NewAttr>: RenderHtml,
230 {
231 self
232 }
233}
234
235impl RenderHtml for TitleView {
236 type AsyncOutput = Self;
237
238 const MIN_LENGTH: usize = 0;
239
240 fn dry_resolve(&mut self) {}
241
242 async fn resolve(self) -> Self::AsyncOutput {
243 self
244 }
245
246 fn to_html_with_buf(
247 self,
248 _buf: &mut String,
249 _position: &mut Position,
250 _escape: bool,
251 _mark_branches: bool,
252 ) {
253 }
256
257 fn hydrate<const FROM_SERVER: bool>(
258 mut self,
259 _cursor: &Cursor,
260 _position: &PositionState,
261 ) -> Self::State {
262 let el = self.el();
263 let meta = self.meta;
264 if let Some(formatter) = self.formatter.take() {
265 *meta.title.formatter.write().or_poisoned() = Some(formatter);
266 }
267 if let Some(text) = self.text.take() {
268 *meta.title.text.write().or_poisoned() = Some(text);
269 }
270 let effect = RenderEffect::new({
271 let el = el.clone();
272 move |prev| {
273 let text = meta.title.as_string().unwrap_or_default();
274
275 if prev.is_some() && prev.as_ref() != Some(&text) {
277 el.set_text_content(Some(&text));
278 }
279
280 text
281 }
282 });
283 TitleViewState { effect }
284 }
285}
286
287impl Mountable for TitleViewState {
288 fn unmount(&mut self) {}
289
290 fn mount(
291 &mut self,
292 _parent: &leptos::tachys::renderer::types::Element,
293 _marker: Option<&leptos::tachys::renderer::types::Node>,
294 ) {
295 }
298
299 fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
300 false
301 }
302}