completing work on meta
This commit is contained in:
parent
72b43d1e2b
commit
2fefc8b4bf
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
289
meta/src/lib.rs
289
meta/src/lib.rs
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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())),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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())),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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())),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue