completing work on meta

This commit is contained in:
Greg Johnston 2024-03-13 19:55:23 -04:00
parent 72b43d1e2b
commit 2fefc8b4bf
9 changed files with 469 additions and 236 deletions

View File

@ -10,11 +10,12 @@ rust-version.workspace = true
[dependencies]
leptos = { workspace = true }
once_cell = "1"
or_poisoned = { workspace = true }
tracing = "0.1"
wasm-bindgen = "0.2"
indexmap = "2"
send_wrapper = "0.6.0"
tracing = "0.1"
wasm-bindgen = "0.2"
[dependencies.web-sys]
version = "0.3"

View File

@ -113,12 +113,15 @@ impl Render<Dom> for HtmlView {
impl RenderHtml<Dom> for HtmlView {
const MIN_LENGTH: usize = 0;
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {}
fn to_html_with_buf(self, _buf: &mut String, _position: &mut Position) {
// meta tags are rendered into the buffer stored into the context
// the value has already been taken out, when we're on the server
}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor<Dom>,
position: &PositionState,
_cursor: &Cursor<Dom>,
_position: &PositionState,
) -> Self::State {
let el = document()
.document_element()
@ -139,15 +142,17 @@ impl Mountable<Dom> for HtmlViewState {
fn mount(
&mut self,
parent: &<Dom as Renderer>::Element,
marker: Option<&<Dom as Renderer>::Node>,
_parent: &<Dom as Renderer>::Element,
_marker: Option<&<Dom as Renderer>::Node>,
) {
// <Html> only sets attributes
// the <html> tag doesn't need to be mounted anywhere, of course
}
fn insert_before_this(
&self,
parent: &<Dom as Renderer>::Element,
child: &mut dyn Mountable<Dom>,
_parent: &<Dom as Renderer>::Element,
_child: &mut dyn Mountable<Dom>,
) -> bool {
true
}

View File

@ -49,50 +49,102 @@
use indexmap::IndexMap;
use leptos::{
debug_warn,
component, debug_warn,
reactive_graph::owner::{provide_context, use_context},
tachys::{
html::attribute::any_attribute::AnyAttribute, renderer::dom::Dom,
dom::document,
html::{
attribute::{any_attribute::AnyAttribute, Attribute},
element::{CreateElement, ElementType, HtmlElement},
},
hydration::Cursor,
renderer::{dom::Dom, Renderer},
view::{Mountable, Position, PositionState, Render, RenderHtml},
},
IntoView,
};
use once_cell::sync::Lazy;
use or_poisoned::OrPoisoned;
use send_wrapper::SendWrapper;
use std::{
cell::{Cell, RefCell},
fmt::Debug,
rc::Rc,
sync::{Arc, RwLock},
};
#[cfg(any(feature = "csr", feature = "hydrate"))]
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use wasm_bindgen::JsCast;
use web_sys::{HtmlHeadElement, Node};
mod body;
mod html;
/*mod link;
mod link;
mod meta_tags;
mod script;
mod style;
mod stylesheet;*/
mod stylesheet;
mod title;
pub use body::*;
pub use html::*;
/*pub use link::*;
pub use link::*;
pub use meta_tags::*;
pub use script::*;
pub use style::*;
pub use stylesheet::*;*/
pub use stylesheet::*;
pub use title::*;
/// Contains the current state of meta tags. To access it, you can use [`use_head`].
///
/// This should generally by provided somewhere in the root of your application using
/// [`provide_meta_context`].
#[derive(Clone, Default, Debug)]
#[derive(Clone, Debug)]
pub struct MetaContext {
/// Metadata associated with the `<title>` element.
pub title: TitleContext,
/*
/// Other metadata tags.
pub tags: MetaTagsContext,
*/
pub(crate) title: TitleContext,
/// The hydration cursor for the location in the `<head>` for arbitrary tags will be rendered.
pub(crate) cursor: Arc<Lazy<SendWrapper<Cursor<Dom>>>>,
}
impl MetaContext {
/// Creates an empty [`MetaContext`].
pub fn new() -> Self {
Default::default()
}
}
pub(crate) const HEAD_MARKER_COMMENT: &str = "HEAD";
/// Return value of [`Node::node_type`] for a comment.
/// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType#node.comment_node
const COMMENT_NODE: u16 = 8;
impl Default for MetaContext {
fn default() -> Self {
let build_cursor: fn() -> SendWrapper<Cursor<Dom>> = || {
let head = document().head().expect("missing <head> element");
let mut cursor = None;
let mut child = head.first_child();
while let Some(this_child) = child {
if this_child.node_type() == COMMENT_NODE
&& this_child.text_content().as_deref()
== Some(HEAD_MARKER_COMMENT)
{
cursor = Some(this_child);
break;
}
child = this_child.next_sibling();
}
SendWrapper::new(Cursor::new(
cursor
.expect("no leptos_meta HEAD marker comment found")
.unchecked_into(),
))
};
let cursor = Arc::new(Lazy::new(build_cursor));
Self {
title: Default::default(),
cursor,
}
}
}
/// Contains the state of meta tags for server rendering.
@ -115,10 +167,8 @@ struct ServerMetaContextInner {
pub(crate) html: Vec<AnyAttribute<Dom>>,
/// Metadata associated with the `<body>` element
pub(crate) body: Vec<AnyAttribute<Dom>>,
/*
/// Other metadata tags.
pub tags: MetaTagsContext,
*/
/// HTML for arbitrary tags that will be included in the `<head>` element
pub(crate) head_html: String,
}
impl Debug for ServerMetaContext {
@ -169,12 +219,207 @@ pub fn use_head() -> MetaContext {
}
}
impl MetaContext {
/// Creates an empty [`MetaContext`].
pub fn new() -> Self {
Default::default()
pub(crate) fn register<E, At, Ch>(
el: HtmlElement<E, At, Ch, Dom>,
) -> RegisteredMetaTag<E, At, Ch>
where
HtmlElement<E, At, Ch, Dom>: RenderHtml<Dom>,
{
let mut el = Some(el);
if let Some(cx) = use_context::<ServerMetaContext>() {
let mut inner = cx.inner.write().or_poisoned();
el.take()
.unwrap()
.to_html_with_buf(&mut inner.head_html, &mut Position::NextChild);
}
RegisteredMetaTag { el }
}
struct RegisteredMetaTag<E, At, Ch> {
// this is `None` if we've already taken it out to render to HTML on the server
// we don't render it in place in RenderHtml, so it's fine
el: Option<HtmlElement<E, At, Ch, Dom>>,
}
struct RegisteredMetaTagState<E, At, Ch>
where
HtmlElement<E, At, Ch, Dom>: Render<Dom>,
{
state: <HtmlElement<E, At, Ch, Dom> as Render<Dom>>::State,
}
fn document_head() -> HtmlHeadElement {
let document = document();
document.head().unwrap_or_else(|| {
let el = document.create_element("head").unwrap();
let document = document.document_element().unwrap();
document.append_child(&el);
el.unchecked_into()
})
}
impl<E, At, Ch> Render<Dom> for RegisteredMetaTag<E, At, Ch>
where
E: CreateElement<Dom>,
At: Attribute<Dom>,
Ch: Render<Dom>,
{
type State = RegisteredMetaTagState<E, At, Ch>;
type FallibleState = RegisteredMetaTagState<E, At, Ch>;
fn build(self) -> Self::State {
let state = self.el.unwrap().build();
RegisteredMetaTagState { state }
}
fn rebuild(self, state: &mut Self::State) {
self.el.unwrap().rebuild(&mut state.state);
}
fn try_build(self) -> leptos::tachys::error::Result<Self::FallibleState> {
Ok(self.build())
}
fn try_rebuild(
self,
state: &mut Self::FallibleState,
) -> leptos::tachys::error::Result<()> {
self.rebuild(state);
Ok(())
}
}
impl<E, At, Ch> RenderHtml<Dom> for RegisteredMetaTag<E, At, Ch>
where
E: ElementType + CreateElement<Dom>,
At: Attribute<Dom>,
Ch: RenderHtml<Dom>,
{
const MIN_LENGTH: usize = 0;
fn to_html_with_buf(self, _buf: &mut String, _position: &mut Position) {
// meta tags are rendered into the buffer stored into the context
// the value has already been taken out, when we're on the server
}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor<Dom>,
position: &PositionState,
) -> Self::State {
let cursor = use_context::<MetaContext>()
.expect(
"attempting to hydrate `leptos_meta` components without a \
MetaContext provided",
)
.cursor;
let state = self.el.unwrap().hydrate::<FROM_SERVER>(
&*cursor,
&PositionState::new(Position::NextChild),
);
RegisteredMetaTagState { state }
}
}
impl<E, At, Ch> Mountable<Dom> for RegisteredMetaTagState<E, At, Ch>
where
E: CreateElement<Dom>,
At: Attribute<Dom>,
Ch: Render<Dom>,
{
fn unmount(&mut self) {
self.state.unmount();
}
fn mount(
&mut self,
_parent: &<Dom as Renderer>::Element,
_marker: Option<&<Dom as Renderer>::Node>,
) {
// we always mount this to the <head>, which is the whole point
// but this shouldn't warn about the parent being a regular element or being unused
// because it will call "mount" with the parent where it is located in the component tree,
// but actually be mounted to the <head>
self.state.mount(&document_head(), None);
}
fn insert_before_this(
&self,
parent: &<Dom as Renderer>::Element,
child: &mut dyn Mountable<Dom>,
) -> bool {
self.state.insert_before_this(&document_head(), child)
}
}
/// During server rendering, inserts the meta tags that have been generated by the other components
/// in this crate into the DOM. This should be placed somewhere inside the `<head>` element that is
/// being used during server rendering.
#[component]
pub fn MetaTags() -> impl IntoView {
MetaTagsView {
context: use_context::<ServerMetaContext>().expect(
"before using the <MetaTags/> component, you should make sure to \
provide ServerMetaContext via context",
),
}
}
struct MetaTagsView {
context: ServerMetaContext,
}
// this implementation doesn't do anything during client-side rendering, it's just for server-side
// rendering HTML for all the tags that will be injected into the `<head>`
//
// client-side rendering is handled by the individual components
impl Render<Dom> for MetaTagsView {
type State = ();
type FallibleState = ();
fn build(self) -> Self::State {}
fn rebuild(self, state: &mut Self::State) {}
fn try_build(self) -> leptos::tachys::error::Result<Self::FallibleState> {
Ok(())
}
fn try_rebuild(
self,
state: &mut Self::FallibleState,
) -> leptos::tachys::error::Result<()> {
Ok(())
}
}
impl RenderHtml<Dom> for MetaTagsView {
const MIN_LENGTH: usize = 0;
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
if let Some(title) = self.context.title.as_string() {
buf.reserve(15 + title.len());
buf.push_str("<title>");
buf.push_str(&title);
buf.push_str("</title>");
}
buf.push_str(&self.context.inner.write().or_poisoned().head_html);
}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor<Dom>,
position: &PositionState,
) -> Self::State {
}
}
impl MetaContext {
// TODO remove the below?
#[cfg(feature = "ssr")]
/// Converts the existing metadata tags into HTML that can be injected into the document head.
///

View File

@ -1,5 +1,14 @@
use crate::use_head;
use leptos::{nonce::use_nonce, *};
use crate::register;
use leptos::{
component,
oco::Oco,
prelude::GlobalAttributes,
tachys::{
html::{attribute::any_attribute::AnyAttribute, element::link},
renderer::dom::Dom,
},
IntoView,
};
/// Injects an [`HTMLLinkElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document
/// head, accepting any of the valid attributes for that tag.
@ -24,7 +33,7 @@ use leptos::{nonce::use_nonce, *};
/// }
/// }
/// ```
#[component(transparent)]
#[component]
pub fn Link(
/// The [`id`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-id) attribute.
#[prop(optional, into)]
@ -35,9 +44,6 @@ pub fn Link(
/// The [`crossorigin`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-crossorigin) attribute.
#[prop(optional, into)]
crossorigin: Option<Oco<'static, str>>,
/// 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<Oco<'static, str>>,
@ -59,9 +65,6 @@ pub fn Link(
/// The [`media`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-media) attribute.
#[prop(optional, into)]
media: Option<Oco<'static, str>>,
/// The [`prefetch`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-prefetch) attribute.
#[prop(optional, into)]
prefetch: Option<Oco<'static, str>>,
/// The [`referrerpolicy`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-referrerpolicy) attribute.
#[prop(optional, into)]
referrerpolicy: Option<Oco<'static, str>>,
@ -82,42 +85,26 @@ pub fn Link(
blocking: Option<Oco<'static, str>>,
/// Custom attributes.
#[prop(attrs, optional)]
attrs: Vec<(&'static str, Attribute)>,
attrs: Vec<AnyAttribute<Dom>>,
) -> impl IntoView {
let meta = use_head();
let next_id = meta.tags.get_next_id();
let mut id: Oco<'static, str> =
id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0).into());
let builder_el = leptos::leptos_dom::html::as_meta_tag({
let id = id.clone_inplace();
move || {
attrs
.into_iter()
.fold(leptos::leptos_dom::html::link(), |el, (name, value)| {
el.attr(name, value)
})
.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)
.attr("nonce", use_nonce())
}
});
meta.tags.register(id, builder_el.into_any());
// TODO additional attributes
register(
link()
.id(id)
.r#as(as_)
.crossorigin(crossorigin)
.fetchpriority(fetchpriority)
.href(href)
.hreflang(hreflang)
.imagesizes(imagesizes)
.imagesrcset(imagesrcset)
.integrity(integrity)
.media(media)
.referrerpolicy(referrerpolicy)
.rel(rel)
.sizes(sizes)
.title(title)
.r#type(type_)
.blocking(blocking),
)
}

View File

@ -1,5 +1,14 @@
use crate::{use_head, TextProp};
use leptos::{component, Attribute, IntoView};
use crate::register;
use leptos::{
component,
prelude::{CustomAttribute, GlobalAttributes},
tachys::{
html::{attribute::any_attribute::AnyAttribute, element::meta},
renderer::dom::Dom,
},
text_prop::TextProp,
IntoView,
};
/// Injects an [`HTMLMetaElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMetaElement) into the document
/// head to set metadata
@ -21,7 +30,7 @@ use leptos::{component, Attribute, IntoView};
/// }
/// }
/// ```
#[component(transparent)]
#[component]
pub fn Meta(
/// The [`charset`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-charset) attribute.
#[prop(optional, into)]
@ -35,29 +44,24 @@ pub fn Meta(
/// The [`http-equiv`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-http-equiv) attribute.
#[prop(optional, into)]
http_equiv: Option<TextProp>,
/// The [`itemprop`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-itemprop) attribute.
#[prop(optional, into)]
itemprop: Option<TextProp>,
/// The [`content`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-content) attribute.
#[prop(optional, into)]
content: Option<TextProp>,
/// Custom attributes.
#[prop(attrs, optional)]
attrs: Vec<(&'static str, Attribute)>,
attrs: Vec<AnyAttribute<Dom>>,
) -> impl IntoView {
let meta = use_head();
let next_id = meta.tags.get_next_id();
let id = format!("leptos-link-{}", next_id.0);
let builder_el = leptos::leptos_dom::html::as_meta_tag(move || {
attrs
.into_iter()
.fold(leptos::leptos_dom::html::meta(), |el, (name, value)| {
el.attr(name, value)
})
.attr("charset", move || charset.as_ref().map(|v| v.get()))
.attr("name", move || name.as_ref().map(|v| v.get()))
.attr("property", move || property.as_ref().map(|v| v.get()))
.attr("http-equiv", move || http_equiv.as_ref().map(|v| v.get()))
.attr("content", move || content.as_ref().map(|v| v.get()))
});
meta.tags.register(id.into(), builder_el.into_any());
// TODO other attrs
register(
meta()
.charset(charset.map(|v| move || v.get()))
.name(name.map(|v| move || v.get()))
.attr("property", property.map(|v| move || v.get()))
.http_equiv(http_equiv.map(|v| move || v.get()))
.itemprop(itemprop.map(|v| move || v.get()))
.content(content.map(|v| move || v.get())),
)
}

View File

@ -1,5 +1,15 @@
use crate::use_head;
use leptos::{nonce::use_nonce, *};
use crate::register;
use leptos::{
component,
oco::Oco,
prelude::*,
tachys::{
html::{attribute::any_attribute::AnyAttribute, element::script},
renderer::dom::Dom,
view::any_view::AnyView,
},
IntoView,
};
/// Injects an [`HTMLScriptElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement) into the document
/// head, accepting any of the valid attributes for that tag.
@ -21,7 +31,7 @@ use leptos::{nonce::use_nonce, *};
/// }
/// }
/// ```
#[component(transparent)]
#[component]
pub fn Script(
/// An ID for the `<script>` tag.
#[prop(optional, into)]
@ -61,55 +71,26 @@ pub fn Script(
blocking: Option<Oco<'static, str>>,
/// The content of the `<script>` tag.
#[prop(optional)]
children: Option<Box<dyn FnOnce() -> Fragment>>,
children: Option<Box<dyn FnOnce() -> AnyView<Dom>>>,
/// Custom attributes.
#[prop(attrs, optional)]
attrs: Vec<(&'static str, Attribute)>,
attrs: Vec<AnyAttribute<Dom>>,
) -> impl IntoView {
let meta = use_head();
let next_id = meta.tags.get_next_id();
let mut id: Oco<'static, str> =
id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0).into());
let builder_el = leptos::leptos_dom::html::as_meta_tag({
let id = id.clone_inplace();
move || {
attrs
.into_iter()
.fold(
leptos::leptos_dom::html::script(),
|el, (name, value)| el.attr(name, value),
)
.attr("id", id)
.attr("async", async_)
.attr("crossorigin", crossorigin)
.attr("defer", defer)
.attr("fetchpriority ", fetchpriority)
.attr("integrity", integrity)
.attr("nomodule", nomodule)
.attr("nonce", nonce)
.attr("referrerpolicy", referrerpolicy)
.attr("src", src)
.attr("type", type_)
.attr("blocking", blocking)
.attr("nonce", use_nonce())
}
});
let builder_el = if let Some(children) = children {
let frag = children();
let mut script = String::new();
for node in frag.nodes {
match node {
View::Text(text) => script.push_str(&text.content),
_ => leptos::logging::warn!(
"Only text nodes are supported as children of <Script/>."
),
}
}
builder_el.child(script)
} else {
builder_el
};
meta.tags.register(id, builder_el.into_any());
// TODO other attrs
register(
script()
.id(id)
.r#async(async_)
.crossorigin(crossorigin)
.defer(defer)
.fetchpriority(fetchpriority)
.integrity(integrity)
.nomodule(nomodule)
.nonce(nonce)
.referrerpolicy(referrerpolicy)
.src(src)
.r#type(type_)
.blocking(blocking)
.child(children.map(|c| c())),
)
}

View File

@ -1,5 +1,15 @@
use crate::use_head;
use leptos::{nonce::use_nonce, *};
use crate::register;
use leptos::{
component,
oco::Oco,
prelude::*,
tachys::{
html::{attribute::any_attribute::AnyAttribute, element::style},
renderer::dom::Dom,
view::any_view::AnyView,
},
IntoView,
};
/// Injects an [`HTMLStyleElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLStyleElement) into the document
/// head, accepting any of the valid attributes for that tag.
@ -21,7 +31,7 @@ use leptos::{nonce::use_nonce, *};
/// }
/// }
/// ```
#[component(transparent)]
#[component]
pub fn Style(
/// An ID for the `<script>` tag.
#[prop(optional, into)]
@ -40,47 +50,19 @@ pub fn Style(
blocking: Option<Oco<'static, str>>,
/// The content of the `<style>` tag.
#[prop(optional)]
children: Option<Box<dyn FnOnce() -> Fragment>>,
children: Option<Box<dyn FnOnce() -> AnyView<Dom>>>,
/// Custom attributes.
#[prop(attrs, optional)]
attrs: Vec<(&'static str, Attribute)>,
attrs: Vec<AnyAttribute<Dom>>,
) -> impl IntoView {
let meta = use_head();
let next_id = meta.tags.get_next_id();
let mut id: Oco<'static, str> =
id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0).into());
let builder_el = leptos::leptos_dom::html::as_meta_tag({
let id = id.clone_inplace();
move || {
attrs
.into_iter()
.fold(leptos::leptos_dom::html::style(), |el, (name, value)| {
el.attr(name, value)
})
.attr("id", id)
.attr("media", media)
.attr("nonce", nonce)
.attr("title", title)
.attr("blocking", blocking)
.attr("nonce", use_nonce())
}
});
let builder_el = if let Some(children) = children {
let frag = children();
let mut style = String::new();
for node in frag.nodes {
match node {
View::Text(text) => style.push_str(&text.content),
_ => leptos::logging::warn!(
"Only text nodes are supported as children of <Style/>."
),
}
}
builder_el.child(style)
} else {
builder_el
};
meta.tags.register(id, builder_el.into_any());
// TODO other attributes
register(
style()
.id(id)
.media(media)
.nonce(nonce)
.title(title)
.blocking(blocking)
.child(children.map(|c| c())),
)
}

View File

@ -1,5 +1,12 @@
use crate::Link;
use leptos::*;
use crate::register;
use leptos::{
component,
tachys::{
html::{attribute::any_attribute::AnyAttribute, element::link},
renderer::dom::Dom,
},
IntoView,
};
/// 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.
@ -19,7 +26,7 @@ use leptos::*;
/// }
/// }
/// ```
#[component(transparent)]
#[component]
pub fn Stylesheet(
/// The URL at which the stylesheet is located.
#[prop(into)]
@ -29,15 +36,8 @@ pub fn Stylesheet(
id: Option<String>,
/// Custom attributes.
#[prop(attrs, optional)]
attrs: Vec<(&'static str, Attribute)>,
attrs: Vec<AnyAttribute<Dom>>,
) -> impl IntoView {
if let Some(id) = id {
view! {
<Link id rel="stylesheet" href attrs/>
}
} else {
view! {
<Link rel="stylesheet" href attrs/>
}
}
// TODO additional attributes
register(link().rel("stylesheet").href(href))
}

View File

@ -11,7 +11,7 @@ use leptos::{
error::Result,
hydration::Cursor,
renderer::{dom::Dom, Renderer},
view::{Mountable, PositionState, Render, RenderHtml},
view::{Mountable, Position, PositionState, Render, RenderHtml},
},
text_prop::TextProp,
IntoView,
@ -20,6 +20,7 @@ use or_poisoned::OrPoisoned;
use send_wrapper::SendWrapper;
use std::{
cell::RefCell,
ops::Deref,
rc::Rc,
sync::{Arc, RwLock},
};
@ -138,7 +139,6 @@ pub fn Title(
}
}
#[derive(Debug)]
struct TitleView {
meta: MetaContext,
formatter: Option<Formatter>,
@ -146,40 +146,45 @@ struct TitleView {
}
impl TitleView {
fn el(&self) -> Element {
fn el(&self) -> HtmlTitleElement {
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(),
Ok(Some(title)) => SendWrapper::new(title.unchecked_into()),
_ => {
let el_ref = meta.title.el.clone();
let el = document().create_element("title").unwrap_throw();
let head = document().head().unwrap_throw();
let el_ref = self.meta.title.el.clone();
let el = SendWrapper::new(
document()
.create_element("title")
.unwrap_throw()
.unchecked_into::<HtmlTitleElement>(),
);
let head =
SendWrapper::new(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_ref.write().or_poisoned() = None;
}
});
el.unchecked_into()
el
}
}
};
*el_ref = Some(el.clone().unchecked_into());
*el_ref = Some(el.clone());
el
el.take()
}
}
#[derive(Debug)]
struct TitleViewState {
el: Element,
el: HtmlTitleElement,
formatter: Option<Formatter>,
text: Option<TextProp>,
effect: RenderEffect<Oco<'static, str>>,
@ -192,14 +197,17 @@ impl Render<Dom> for TitleView {
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();
let effect = RenderEffect::new({
let el = el.clone();
move |prev| {
let text = meta.title.as_string().unwrap_or_default();
if prev.as_ref() != Some(&text) {
el.set_text_content(Some(&text));
if prev.as_ref() != Some(&text) {
el.set_text_content(Some(&text));
}
text
}
text
});
TitleViewState {
el,
@ -209,7 +217,7 @@ impl Render<Dom> for TitleView {
}
}
fn rebuild(self, state: &mut Self::State) {
fn rebuild(self, _state: &mut Self::State) {
// TODO should this rebuild?
}
@ -226,19 +234,37 @@ impl Render<Dom> for TitleView {
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 to_html_with_buf(self, buf: &mut String, position: &mut Position) {
// meta tags are rendered into the buffer stored into the context
// the value has already been taken out, when we're on the server
}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor<Dom>,
position: &PositionState,
_cursor: &Cursor<Dom>,
_position: &PositionState,
) -> Self::State {
self.build()
let el = self.el();
let meta = self.meta;
let effect = RenderEffect::new({
let el = el.clone();
move |prev| {
let text = meta.title.as_string().unwrap_or_default();
// don't reset the title on initial hydration
if prev.is_some() && prev.as_ref() != Some(&text) {
el.set_text_content(Some(&text));
}
text
}
});
TitleViewState {
el,
formatter: self.formatter,
text: self.text,
effect,
}
}
}
@ -250,12 +276,14 @@ impl Mountable<Dom> for TitleViewState {
parent: &<Dom as Renderer>::Element,
marker: Option<&<Dom as Renderer>::Node>,
) {
// <title> doesn't need to be mounted
// TitleView::el() guarantees that there is a <title> in the <head>
}
fn insert_before_this(
&self,
parent: &<Dom as Renderer>::Element,
child: &mut dyn Mountable<Dom>,
_parent: &<Dom as Renderer>::Element,
_child: &mut dyn Mountable<Dom>,
) -> bool {
true
}