initial work on meta

This commit is contained in:
Greg Johnston 2024-03-13 08:57:17 -04:00
parent 39607adc94
commit 72b43d1e2b
5 changed files with 336 additions and 323 deletions

View file

@ -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"

View file

@ -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 documents `<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 }
}
}

View file

@ -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 documents `<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
}
}

View file

@ -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 {

View file

@ -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 its 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
}
}