mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 14:54:16 +00:00
initial work on meta
This commit is contained in:
parent
39607adc94
commit
72b43d1e2b
5 changed files with 336 additions and 323 deletions
|
@ -14,6 +14,7 @@ or_poisoned = { workspace = true }
|
|||
tracing = "0.1"
|
||||
wasm-bindgen = "0.2"
|
||||
indexmap = "2"
|
||||
send_wrapper = "0.6.0"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
|
|
115
meta/src/body.rs
115
meta/src/body.rs
|
@ -1,4 +1,4 @@
|
|||
use crate::{use_head, ServerMetaContext};
|
||||
use crate::ServerMetaContext;
|
||||
use indexmap::IndexMap;
|
||||
use leptos::{
|
||||
component,
|
||||
|
@ -27,52 +27,7 @@ use std::{
|
|||
rc::Rc,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
use web_sys::{HtmlBodyElement, HtmlElement};
|
||||
|
||||
/// Contains the current metadata for the document's `<body>`.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct BodyContext {
|
||||
class: Arc<RwLock<Option<TextProp>>>,
|
||||
attributes: Arc<RwLock<Vec<AnyAttribute<Dom>>>>,
|
||||
}
|
||||
|
||||
impl BodyContext {
|
||||
/// Converts the `<body>` metadata into an HTML string.
|
||||
///
|
||||
/// This consumes the list of `attributes`, and should only be called once per request.
|
||||
pub fn to_string(&self) -> Option<String> {
|
||||
let mut buf = String::from(" ");
|
||||
if let Some(class) = &*self.class.read().or_poisoned() {
|
||||
buf.push_str("class=\"");
|
||||
buf.push_str(&class.get());
|
||||
buf.push_str("\" ");
|
||||
};
|
||||
|
||||
let attributes = mem::take(&mut *self.attributes.write().or_poisoned());
|
||||
|
||||
for attr in attributes {
|
||||
attr.to_html(
|
||||
&mut buf,
|
||||
&mut String::new(),
|
||||
&mut String::new(),
|
||||
&mut String::new(),
|
||||
);
|
||||
buf.push(' ');
|
||||
}
|
||||
|
||||
if buf.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for BodyContext {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("BodyContext").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
/// A component to set metadata on the document’s `<body>` element from
|
||||
/// within the application.
|
||||
|
@ -102,35 +57,27 @@ impl core::fmt::Debug for BodyContext {
|
|||
/// ```
|
||||
#[component]
|
||||
pub fn Body(
|
||||
/// The `class` attribute on the `<body>`.
|
||||
#[prop(optional, into)]
|
||||
class: Option<TextProp>,
|
||||
/// The `id` attribute on the `<body>`.
|
||||
#[prop(optional, into)]
|
||||
id: Option<TextProp>,
|
||||
/// Arbitrary attributes to add to the `<body>`
|
||||
/// Arbitrary attributes to add to the `<body>`.
|
||||
#[prop(attrs)]
|
||||
mut attributes: Vec<AnyAttribute<Dom>>,
|
||||
) -> impl IntoView {
|
||||
if let Some(meta) = use_context::<ServerMetaContext>() {
|
||||
*meta.body.class.write().or_poisoned() = class.clone();
|
||||
|
||||
// these can safely be taken out if the server context is present
|
||||
// server rendering is handled separately, not via RenderHtml
|
||||
*meta.body.attributes.write().or_poisoned() = mem::take(&mut attributes)
|
||||
let mut meta = meta.inner.write().or_poisoned();
|
||||
// if we are server rendering, we will not actually use these values via RenderHtml
|
||||
// instead, they'll be handled separately by the server integration
|
||||
// so it's safe to take them out of the props here
|
||||
meta.body = mem::take(&mut attributes);
|
||||
}
|
||||
|
||||
BodyView { class, attributes }
|
||||
BodyView { attributes }
|
||||
}
|
||||
|
||||
struct BodyView {
|
||||
class: Option<TextProp>,
|
||||
attributes: Vec<AnyAttribute<Dom>>,
|
||||
}
|
||||
|
||||
struct BodyViewState {
|
||||
el: HtmlElement,
|
||||
class: Option<RenderEffect<Oco<'static, str>>>,
|
||||
attributes: Vec<AnyAttributeState<Dom>>,
|
||||
}
|
||||
|
||||
|
@ -140,20 +87,6 @@ impl Render<Dom> for BodyView {
|
|||
|
||||
fn build(self) -> Self::State {
|
||||
let el = document().body().expect("there to be a <body> element");
|
||||
let class = self.class.map(|class| {
|
||||
RenderEffect::new({
|
||||
let el = el.clone();
|
||||
move |prev| {
|
||||
let next = class.get();
|
||||
if prev.as_ref() != Some(&next) {
|
||||
if let Err(e) = el.set_attribute("class", &next) {
|
||||
web_sys::console::error_1(&e);
|
||||
}
|
||||
}
|
||||
next
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let attributes = self
|
||||
.attributes
|
||||
|
@ -161,11 +94,7 @@ impl Render<Dom> for BodyView {
|
|||
.map(|attr| attr.build(&el))
|
||||
.collect();
|
||||
|
||||
BodyViewState {
|
||||
el,
|
||||
class,
|
||||
attributes,
|
||||
}
|
||||
BodyViewState { el, attributes }
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
|
@ -193,24 +122,6 @@ impl RenderHtml<Dom> for BodyView {
|
|||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
let el = document().body().expect("there to be a <body> element");
|
||||
let class = self.class.map(|class| {
|
||||
RenderEffect::new({
|
||||
let el = el.clone();
|
||||
move |prev| {
|
||||
let next = class.get();
|
||||
if prev.is_none() {
|
||||
return next;
|
||||
}
|
||||
|
||||
if prev.as_ref() != Some(&next) {
|
||||
if let Err(e) = el.set_attribute("class", &next) {
|
||||
web_sys::console::error_1(&e);
|
||||
}
|
||||
}
|
||||
next
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let attributes = self
|
||||
.attributes
|
||||
|
@ -218,11 +129,7 @@ impl RenderHtml<Dom> for BodyView {
|
|||
.map(|attr| attr.hydrate::<FROM_SERVER>(&el))
|
||||
.collect();
|
||||
|
||||
BodyViewState {
|
||||
el,
|
||||
class,
|
||||
attributes,
|
||||
}
|
||||
BodyViewState { el, attributes }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
255
meta/src/html.rs
255
meta/src/html.rs
|
@ -1,80 +1,33 @@
|
|||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::collections::HashMap;
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
/// Contains the current metadata for the document's `<html>`.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct HtmlContext {
|
||||
#[cfg(feature = "ssr")]
|
||||
lang: Rc<RefCell<Option<TextProp>>>,
|
||||
#[cfg(feature = "ssr")]
|
||||
dir: Rc<RefCell<Option<TextProp>>>,
|
||||
#[cfg(feature = "ssr")]
|
||||
class: Rc<RefCell<Option<TextProp>>>,
|
||||
#[cfg(feature = "ssr")]
|
||||
attributes: Rc<RefCell<HashMap<&'static str, Attribute>>>,
|
||||
}
|
||||
|
||||
impl HtmlContext {
|
||||
/// Converts the `<html>` metadata into an HTML string.
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
pub fn as_string(&self) -> Option<String> {
|
||||
let lang = self.lang.borrow().as_ref().map(|val| {
|
||||
format!(
|
||||
"lang=\"{}\"",
|
||||
leptos::leptos_dom::ssr::escape_attr(&val.get())
|
||||
)
|
||||
});
|
||||
let dir = self.dir.borrow().as_ref().map(|val| {
|
||||
format!(
|
||||
"dir=\"{}\"",
|
||||
leptos::leptos_dom::ssr::escape_attr(&val.get())
|
||||
)
|
||||
});
|
||||
let class = self.class.borrow().as_ref().map(|val| {
|
||||
format!(
|
||||
"class=\"{}\"",
|
||||
leptos::leptos_dom::ssr::escape_attr(&val.get())
|
||||
)
|
||||
});
|
||||
let attributes = self.attributes.borrow();
|
||||
let attributes = (!attributes.is_empty()).then(|| {
|
||||
attributes
|
||||
.iter()
|
||||
.filter_map(|(n, v)| {
|
||||
v.as_nameless_value_string().map(|v| {
|
||||
format!(
|
||||
"{}=\"{}\"",
|
||||
n,
|
||||
leptos::leptos_dom::ssr::escape_attr(&v)
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
});
|
||||
let mut val = [lang, dir, class, attributes]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
if val.is_empty() {
|
||||
None
|
||||
} else {
|
||||
val.insert(0, ' ');
|
||||
Some(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for HtmlContext {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_tuple("TitleContext").finish()
|
||||
}
|
||||
}
|
||||
use crate::ServerMetaContext;
|
||||
use indexmap::IndexMap;
|
||||
use leptos::{
|
||||
component,
|
||||
oco::Oco,
|
||||
reactive_graph::{effect::RenderEffect, owner::use_context},
|
||||
tachys::{
|
||||
dom::document,
|
||||
error::Result,
|
||||
html::attribute::{
|
||||
any_attribute::{AnyAttribute, AnyAttributeState},
|
||||
Attribute,
|
||||
},
|
||||
hydration::Cursor,
|
||||
reactive_graph::RenderEffectState,
|
||||
renderer::{dom::Dom, Renderer},
|
||||
view::{Mountable, Position, PositionState, Render, RenderHtml},
|
||||
},
|
||||
text_prop::TextProp,
|
||||
IntoView,
|
||||
};
|
||||
use or_poisoned::OrPoisoned;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::HashMap,
|
||||
mem,
|
||||
rc::Rc,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
use web_sys::{Element, HtmlElement};
|
||||
|
||||
/// A component to set metadata on the document’s `<html>` element from
|
||||
/// within the application.
|
||||
|
@ -99,73 +52,103 @@ impl core::fmt::Debug for HtmlContext {
|
|||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[component(transparent)]
|
||||
#[component]
|
||||
pub fn Html(
|
||||
/// The `lang` attribute on the `<html>`.
|
||||
#[prop(optional, into)]
|
||||
lang: Option<TextProp>,
|
||||
/// The `dir` attribute on the `<html>`.
|
||||
#[prop(optional, into)]
|
||||
dir: Option<TextProp>,
|
||||
/// The `class` attribute on the `<html>`.
|
||||
#[prop(optional, into)]
|
||||
class: Option<TextProp>,
|
||||
/// Arbitrary attributes to add to the `<html>`
|
||||
#[prop(attrs)]
|
||||
attributes: Vec<(&'static str, Attribute)>,
|
||||
mut attributes: Vec<AnyAttribute<Dom>>,
|
||||
) -> impl IntoView {
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", any(feature = "csr", feature = "hydrate")))] {
|
||||
use wasm_bindgen::JsCast;
|
||||
if let Some(meta) = use_context::<ServerMetaContext>() {
|
||||
let mut meta = meta.inner.write().or_poisoned();
|
||||
// if we are server rendering, we will not actually use these values via RenderHtml
|
||||
// instead, they'll be handled separately by the server integration
|
||||
// so it's safe to take them out of the props here
|
||||
meta.html = mem::take(&mut attributes);
|
||||
}
|
||||
|
||||
let el = document().document_element().expect("there to be a <html> element");
|
||||
HtmlView { attributes }
|
||||
}
|
||||
|
||||
if let Some(lang) = lang {
|
||||
let el = el.clone();
|
||||
create_render_effect(move |_| {
|
||||
let value = lang.get();
|
||||
_ = el.set_attribute("lang", &value);
|
||||
});
|
||||
}
|
||||
struct HtmlView {
|
||||
attributes: Vec<AnyAttribute<Dom>>,
|
||||
}
|
||||
|
||||
if let Some(dir) = dir {
|
||||
let el = el.clone();
|
||||
create_render_effect(move |_| {
|
||||
let value = dir.get();
|
||||
_ = el.set_attribute("dir", &value);
|
||||
});
|
||||
}
|
||||
struct HtmlViewState {
|
||||
el: Element,
|
||||
attributes: Vec<AnyAttributeState<Dom>>,
|
||||
}
|
||||
|
||||
if let Some(class) = class {
|
||||
let el = el.clone();
|
||||
create_render_effect(move |_| {
|
||||
let value = class.get();
|
||||
_ = el.set_attribute("class", &value);
|
||||
});
|
||||
}
|
||||
impl Render<Dom> for HtmlView {
|
||||
type State = HtmlViewState;
|
||||
type FallibleState = HtmlViewState;
|
||||
|
||||
for (name, value) in attributes {
|
||||
leptos::leptos_dom::attribute_helper(el.unchecked_ref(), name.into(), value);
|
||||
}
|
||||
} else if #[cfg(feature = "ssr")] {
|
||||
let meta = crate::use_head();
|
||||
if lang.is_some() {
|
||||
*meta.html.lang.borrow_mut() = lang;
|
||||
}
|
||||
if dir.is_some() {
|
||||
*meta.html.dir.borrow_mut() = dir;
|
||||
}
|
||||
if class.is_some() {
|
||||
*meta.html.class.borrow_mut() = class;
|
||||
}
|
||||
meta.html.attributes.borrow_mut().extend(attributes);
|
||||
} else {
|
||||
_ = lang;
|
||||
_ = dir;
|
||||
_ = class;
|
||||
_ = attributes;
|
||||
#[cfg(debug_assertions)]
|
||||
crate::feature_warning();
|
||||
}
|
||||
fn build(self) -> Self::State {
|
||||
let el = document()
|
||||
.document_element()
|
||||
.expect("there to be a <html> element");
|
||||
|
||||
let attributes = self
|
||||
.attributes
|
||||
.into_iter()
|
||||
.map(|attr| attr.build(&el))
|
||||
.collect();
|
||||
|
||||
HtmlViewState { el, attributes }
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
// TODO rebuilding dynamic things like this
|
||||
}
|
||||
|
||||
fn try_build(self) -> Result<Self::FallibleState> {
|
||||
Ok(self.build())
|
||||
}
|
||||
|
||||
fn try_rebuild(self, state: &mut Self::FallibleState) -> Result<()> {
|
||||
self.rebuild(state);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderHtml<Dom> for HtmlView {
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
cursor: &Cursor<Dom>,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
let el = document()
|
||||
.document_element()
|
||||
.expect("there to be a <html> element");
|
||||
|
||||
let attributes = self
|
||||
.attributes
|
||||
.into_iter()
|
||||
.map(|attr| attr.hydrate::<FROM_SERVER>(&el))
|
||||
.collect();
|
||||
|
||||
HtmlViewState { el, attributes }
|
||||
}
|
||||
}
|
||||
|
||||
impl Mountable<Dom> for HtmlViewState {
|
||||
fn unmount(&mut self) {}
|
||||
|
||||
fn mount(
|
||||
&mut self,
|
||||
parent: &<Dom as Renderer>::Element,
|
||||
marker: Option<&<Dom as Renderer>::Node>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn insert_before_this(
|
||||
&self,
|
||||
parent: &<Dom as Renderer>::Element,
|
||||
child: &mut dyn Mountable<Dom>,
|
||||
) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,31 +51,35 @@ use indexmap::IndexMap;
|
|||
use leptos::{
|
||||
debug_warn,
|
||||
reactive_graph::owner::{provide_context, use_context},
|
||||
tachys::{
|
||||
html::attribute::any_attribute::AnyAttribute, renderer::dom::Dom,
|
||||
},
|
||||
};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
fmt::Debug,
|
||||
rc::Rc,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
|
||||
mod body;
|
||||
/*mod html;
|
||||
mod link;
|
||||
mod html;
|
||||
/*mod link;
|
||||
mod meta_tags;
|
||||
mod script;
|
||||
mod style;
|
||||
mod stylesheet;
|
||||
mod title;*/
|
||||
mod stylesheet;*/
|
||||
mod title;
|
||||
pub use body::*;
|
||||
/*pub use html::*;
|
||||
pub use link::*;
|
||||
pub use html::*;
|
||||
/*pub use link::*;
|
||||
pub use meta_tags::*;
|
||||
pub use script::*;
|
||||
pub use style::*;
|
||||
pub use stylesheet::*;
|
||||
pub use title::*;*/
|
||||
pub use stylesheet::*;*/
|
||||
pub use title::*;
|
||||
|
||||
/// Contains the current state of meta tags. To access it, you can use [`use_head`].
|
||||
///
|
||||
|
@ -83,10 +87,8 @@ pub use title::*;*/
|
|||
/// [`provide_meta_context`].
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct MetaContext {
|
||||
/*/// Metadata associated with the `<html>` element
|
||||
pub html: HtmlContext,
|
||||
/// Metadata associated with the `<title>` element.
|
||||
pub title: TitleContext,*/
|
||||
pub title: TitleContext,
|
||||
/*
|
||||
/// Other metadata tags.
|
||||
pub tags: MetaTagsContext,
|
||||
|
@ -96,20 +98,35 @@ pub struct MetaContext {
|
|||
/// Contains the state of meta tags for server rendering.
|
||||
///
|
||||
/// This should be provided as context during server rendering.
|
||||
#[derive(Clone, Default, Debug)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct ServerMetaContext {
|
||||
inner: Arc<RwLock<ServerMetaContextInner>>,
|
||||
/// Metadata associated with the `<title>` element.
|
||||
pub(crate) title: TitleContext,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ServerMetaContextInner {
|
||||
/*/// Metadata associated with the `<html>` element
|
||||
pub html: HtmlContext,
|
||||
/// Metadata associated with the `<title>` element.
|
||||
pub title: TitleContext,*/
|
||||
/// Metadata associated with the `<html>` element
|
||||
pub(crate) html: Vec<AnyAttribute<Dom>>,
|
||||
/// Metadata associated with the `<body>` element
|
||||
pub(crate) body: BodyContext,
|
||||
pub(crate) body: Vec<AnyAttribute<Dom>>,
|
||||
/*
|
||||
/// Other metadata tags.
|
||||
pub tags: MetaTagsContext,
|
||||
*/
|
||||
}
|
||||
|
||||
impl Debug for ServerMetaContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ServerMetaContext").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerMetaContext {
|
||||
/// Creates an empty [`ServerMetaContext`].
|
||||
pub fn new() -> Self {
|
||||
|
|
|
@ -1,25 +1,45 @@
|
|||
use crate::use_head;
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
use crate::{use_head, MetaContext, ServerMetaContext};
|
||||
use leptos::{
|
||||
component,
|
||||
oco::Oco,
|
||||
reactive_graph::{
|
||||
effect::RenderEffect,
|
||||
owner::{use_context, Owner},
|
||||
},
|
||||
tachys::{
|
||||
dom::document,
|
||||
error::Result,
|
||||
hydration::Cursor,
|
||||
renderer::{dom::Dom, Renderer},
|
||||
view::{Mountable, PositionState, Render, RenderHtml},
|
||||
},
|
||||
text_prop::TextProp,
|
||||
IntoView,
|
||||
};
|
||||
use or_poisoned::OrPoisoned;
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
rc::Rc,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
use web_sys::{Element, HtmlTitleElement};
|
||||
|
||||
/// 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>>>,
|
||||
el: Arc<RwLock<Option<SendWrapper<HtmlTitleElement>>>>,
|
||||
formatter: Arc<RwLock<Option<Formatter>>>,
|
||||
text: Arc<RwLock<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);
|
||||
let title = self.text.read().or_poisoned().as_ref().map(TextProp::get);
|
||||
title.map(|title| {
|
||||
if let Some(formatter) = &*self.formatter.borrow() {
|
||||
if let Some(formatter) = &*self.formatter.read().or_poisoned() {
|
||||
(formatter.0)(title.into_owned()).into()
|
||||
} else {
|
||||
title
|
||||
|
@ -36,11 +56,11 @@ impl core::fmt::Debug for TitleContext {
|
|||
|
||||
/// A function that is applied to the text value before setting `document.title`.
|
||||
#[repr(transparent)]
|
||||
pub struct Formatter(Box<dyn Fn(String) -> String>);
|
||||
pub struct Formatter(Box<dyn Fn(String) -> String + Send + Sync>);
|
||||
|
||||
impl<F> From<F> for Formatter
|
||||
where
|
||||
F: Fn(String) -> String + 'static,
|
||||
F: Fn(String) -> String + Send + Sync + 'static,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn from(f: F) -> Formatter {
|
||||
|
@ -88,70 +108,155 @@ where
|
|||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[component(transparent)]
|
||||
#[component]
|
||||
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>,
|
||||
mut formatter: Option<Formatter>,
|
||||
/// Sets the current `document.title`.
|
||||
#[prop(optional, into)]
|
||||
text: Option<TextProp>,
|
||||
mut 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);
|
||||
}
|
||||
let server_ctx = use_context::<ServerMetaContext>();
|
||||
if let Some(cx) = server_ctx {
|
||||
// if we are server rendering, we will not actually use these values via RenderHtml
|
||||
// instead, they'll be handled separately by the server integration
|
||||
// so it's safe to take them out of the props here
|
||||
if let Some(formatter) = formatter.take() {
|
||||
*cx.title.formatter.write().or_poisoned() = Some(formatter);
|
||||
}
|
||||
if let Some(text) = text.take() {
|
||||
*cx.title.text.write().or_poisoned() = Some(text);
|
||||
}
|
||||
};
|
||||
|
||||
TitleView {
|
||||
meta,
|
||||
formatter,
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TitleView {
|
||||
meta: MetaContext,
|
||||
formatter: Option<Formatter>,
|
||||
text: Option<TextProp>,
|
||||
}
|
||||
|
||||
impl TitleView {
|
||||
fn el(&self) -> Element {
|
||||
let mut el_ref = self.meta.title.el.write().or_poisoned();
|
||||
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();
|
||||
|
||||
Owner::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
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TitleViewState {
|
||||
el: Element,
|
||||
formatter: Option<Formatter>,
|
||||
text: Option<TextProp>,
|
||||
effect: RenderEffect<Oco<'static, str>>,
|
||||
}
|
||||
|
||||
impl Render<Dom> for TitleView {
|
||||
type State = TitleViewState;
|
||||
type FallibleState = TitleViewState;
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
let el = self.el();
|
||||
let meta = self.meta;
|
||||
let effect = RenderEffect::new(move |prev| {
|
||||
let text = meta.title.as_string().unwrap_or_default();
|
||||
|
||||
if prev.as_ref() != Some(&text) {
|
||||
el.set_text_content(Some(&text));
|
||||
}
|
||||
|
||||
text
|
||||
});
|
||||
TitleViewState {
|
||||
el,
|
||||
formatter: self.formatter,
|
||||
text: self.text,
|
||||
effect,
|
||||
}
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
// TODO should this rebuild?
|
||||
}
|
||||
|
||||
fn try_build(self) -> Result<Self::FallibleState> {
|
||||
Ok(self.build())
|
||||
}
|
||||
|
||||
fn try_rebuild(self, state: &mut Self::FallibleState) -> Result<()> {
|
||||
self.rebuild(state);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderHtml<Dom> for TitleView {
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
fn to_html_with_buf(
|
||||
self,
|
||||
buf: &mut String,
|
||||
position: &mut leptos::tachys::view::Position,
|
||||
) {
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
cursor: &Cursor<Dom>,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
self.build()
|
||||
}
|
||||
}
|
||||
|
||||
impl Mountable<Dom> for TitleViewState {
|
||||
fn unmount(&mut self) {}
|
||||
|
||||
fn mount(
|
||||
&mut self,
|
||||
parent: &<Dom as Renderer>::Element,
|
||||
marker: Option<&<Dom as Renderer>::Node>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn insert_before_this(
|
||||
&self,
|
||||
parent: &<Dom as Renderer>::Element,
|
||||
child: &mut dyn Mountable<Dom>,
|
||||
) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue