feat: correct HTML rendering for spread attributes on <Body/> and <Html/>

This commit is contained in:
Greg Johnston 2024-07-06 12:49:16 -04:00
parent e9c7b50dfd
commit f7b16b726b
3 changed files with 114 additions and 143 deletions

View file

@ -1,18 +1,12 @@
use crate::ServerMetaContext;
use leptos::{
attr::NextAttribute,
component,
html::{self, body, ElementExt},
reactive_graph::owner::use_context,
tachys::{
dom::document,
html::{
attribute::{
any_attribute::{
AnyAttribute, AnyAttributeState, IntoAnyAttribute,
},
Attribute,
},
class,
},
html::attribute::Attribute,
hydration::Cursor,
renderer::{dom::Dom, Renderer},
view::{
@ -20,11 +14,8 @@ use leptos::{
RenderHtml,
},
},
text_prop::TextProp,
IntoView,
};
use or_poisoned::OrPoisoned;
use std::mem;
use web_sys::HtmlElement;
/// A component to set metadata on the documents `<body>` element from
@ -54,84 +45,76 @@ use web_sys::HtmlElement;
/// }
/// ```
#[component]
pub fn Body(
/// The `class` attribute on the `<body>`.
#[prop(optional, into)]
mut class: Option<TextProp>,
/// Arbitrary attributes to add to the `<body>`.
#[prop(attrs)]
mut attributes: Vec<AnyAttribute<Dom>>,
) -> impl IntoView {
if let Some(value) = class.take() {
let value = class::class(move || value.get());
attributes.push(value.into_any_attr());
}
if let Some(meta) = use_context::<ServerMetaContext>() {
// 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
for attr in attributes.drain(0..) {
// fails only if receiver is already dropped
_ = meta.body.send(attr);
}
}
BodyView { attributes }
pub fn Body() -> impl IntoView {
BodyView { attributes: () }
}
struct BodyView {
attributes: Vec<AnyAttribute<Dom>>,
struct BodyView<At> {
attributes: At,
}
#[allow(dead_code)] // TODO these should be used to rebuild the attributes, I guess
struct BodyViewState {
struct BodyViewState<At>
where
At: Attribute<Dom>,
{
el: HtmlElement,
attributes: Vec<AnyAttributeState<Dom>>,
attributes: At::State,
}
impl Render<Dom> for BodyView {
type State = BodyViewState;
impl<At> Render<Dom> for BodyView<At>
where
At: Attribute<Dom>,
{
type State = BodyViewState<At>;
fn build(self) -> Self::State {
let el = document().body().expect("there to be a <body> element");
let attributes = self
.attributes
.into_iter()
.map(|attr| attr.build(&el))
.collect();
let attributes = self.attributes.build(&el);
BodyViewState { el, attributes }
}
fn rebuild(self, _state: &mut Self::State) {
todo!()
fn rebuild(self, state: &mut Self::State) {
self.attributes.rebuild(&mut state.attributes);
}
}
impl AddAnyAttr<Dom> for BodyView {
type Output<SomeNewAttr: Attribute<Dom>> = BodyView;
impl<At> AddAnyAttr<Dom> for BodyView<At>
where
At: Attribute<Dom>,
{
type Output<SomeNewAttr: Attribute<Dom>> =
BodyView<<At as NextAttribute<Dom>>::Output<SomeNewAttr>>;
fn add_any_attr<NewAttr: Attribute<Dom>>(
self,
_attr: NewAttr,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml<Dom>,
{
todo!()
BodyView {
attributes: self.attributes.add_any_attr(attr),
}
}
}
impl RenderHtml<Dom> for BodyView {
type AsyncOutput = Self;
impl<At> RenderHtml<Dom> for BodyView<At>
where
At: Attribute<Dom>,
{
type AsyncOutput = BodyView<At::AsyncOutput>;
const MIN_LENGTH: usize = 0;
const MIN_LENGTH: usize = At::MIN_LENGTH;
fn dry_resolve(&mut self) {}
fn dry_resolve(&mut self) {
self.attributes.dry_resolve();
}
async fn resolve(self) -> Self::AsyncOutput {
self
BodyView {
attributes: self.attributes.resolve().await,
}
}
fn to_html_with_buf(
@ -140,6 +123,13 @@ impl RenderHtml<Dom> for BodyView {
_position: &mut Position,
_escape: bool,
) {
if let Some(meta) = use_context::<ServerMetaContext>() {
let mut buf = String::new();
_ = html::attribute_to_html(self.attributes, &mut buf);
if !buf.is_empty() {
_ = meta.body.send(buf);
}
}
}
fn hydrate<const FROM_SERVER: bool>(
@ -148,18 +138,16 @@ impl RenderHtml<Dom> for BodyView {
_position: &PositionState,
) -> Self::State {
let el = document().body().expect("there to be a <body> element");
let attributes = self
.attributes
.into_iter()
.map(|attr| attr.hydrate::<FROM_SERVER>(&el))
.collect();
let attributes = self.attributes.hydrate::<FROM_SERVER>(&el);
BodyViewState { el, attributes }
}
}
impl Mountable<Dom> for BodyViewState {
impl<At> Mountable<Dom> for BodyViewState<At>
where
At: Attribute<Dom>,
{
fn unmount(&mut self) {}
fn mount(

View file

@ -1,6 +1,7 @@
use crate::ServerMetaContext;
use leptos::{
component,
attr::NextAttribute,
component, html,
reactive_graph::owner::use_context,
tachys::{
dom::document,
@ -52,99 +53,79 @@ use web_sys::Element;
/// }
/// ```
#[component]
pub fn Html(
/// The `lang` attribute on the `<html>`.
#[prop(optional, into)]
mut lang: Option<TextProp>,
/// The `dir` attribute on the `<html>`.
#[prop(optional, into)]
mut dir: Option<TextProp>,
/// The `class` attribute on the `<html>`.
#[prop(optional, into)]
mut class: Option<TextProp>,
/// Arbitrary attributes to add to the `<html>`
#[prop(attrs)]
mut attributes: Vec<AnyAttribute<Dom>>,
) -> impl IntoView {
attributes.extend(
lang.take()
.map(|value| attribute::lang(move || value.get()).into_any_attr())
.into_iter()
.chain(dir.take().map(|value| {
attribute::dir(move || value.get()).into_any_attr()
}))
.chain(class.take().map(|value| {
class::class(move || value.get()).into_any_attr()
})),
);
if let Some(meta) = use_context::<ServerMetaContext>() {
// 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
for attr in attributes.drain(0..) {
// fails only if receiver is already dropped
_ = meta.body.send(attr);
}
}
HtmlView { attributes }
pub fn Html() -> impl IntoView {
HtmlView { attributes: () }
}
struct HtmlView {
attributes: Vec<AnyAttribute<Dom>>,
struct HtmlView<At> {
attributes: At,
}
#[allow(dead_code)] // TODO these should be used to rebuild the attributes, I guess
struct HtmlViewState {
struct HtmlViewState<At>
where
At: Attribute<Dom>,
{
el: Element,
attributes: Vec<AnyAttributeState<Dom>>,
attributes: At::State,
}
impl Render<Dom> for HtmlView {
type State = HtmlViewState;
impl<At> Render<Dom> for HtmlView<At>
where
At: Attribute<Dom>,
{
type State = HtmlViewState<At>;
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();
let attributes = self.attributes.build(&el);
HtmlViewState { el, attributes }
}
fn rebuild(self, _state: &mut Self::State) {
todo!()
fn rebuild(self, state: &mut Self::State) {
self.attributes.rebuild(&mut state.attributes);
}
}
impl AddAnyAttr<Dom> for HtmlView {
type Output<SomeNewAttr: Attribute<Dom>> = HtmlView;
impl<At> AddAnyAttr<Dom> for HtmlView<At>
where
At: Attribute<Dom>,
{
type Output<SomeNewAttr: Attribute<Dom>> =
HtmlView<<At as NextAttribute<Dom>>::Output<SomeNewAttr>>;
fn add_any_attr<NewAttr: Attribute<Dom>>(
self,
_attr: NewAttr,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml<Dom>,
{
todo!()
HtmlView {
attributes: self.attributes.add_any_attr(attr),
}
}
}
impl RenderHtml<Dom> for HtmlView {
type AsyncOutput = Self;
impl<At> RenderHtml<Dom> for HtmlView<At>
where
At: Attribute<Dom>,
{
type AsyncOutput = HtmlView<At::AsyncOutput>;
const MIN_LENGTH: usize = 0;
const MIN_LENGTH: usize = At::MIN_LENGTH;
fn dry_resolve(&mut self) {}
fn dry_resolve(&mut self) {
self.attributes.dry_resolve();
}
async fn resolve(self) -> Self::AsyncOutput {
self
HtmlView {
attributes: self.attributes.resolve().await,
}
}
fn to_html_with_buf(
@ -153,8 +134,13 @@ impl RenderHtml<Dom> for HtmlView {
_position: &mut Position,
_escape: bool,
) {
// meta tags are rendered into the buffer stored into the context
// the value has already been taken out, when we're on the server
if let Some(meta) = use_context::<ServerMetaContext>() {
let mut buf = String::new();
_ = html::attribute_to_html(self.attributes, &mut buf);
if !buf.is_empty() {
_ = meta.html.send(buf);
}
}
}
fn hydrate<const FROM_SERVER: bool>(
@ -166,17 +152,16 @@ impl RenderHtml<Dom> for HtmlView {
.document_element()
.expect("there to be a <html> element");
let attributes = self
.attributes
.into_iter()
.map(|attr| attr.hydrate::<FROM_SERVER>(&el))
.collect();
let attributes = self.attributes.hydrate::<FROM_SERVER>(&el);
HtmlViewState { el, attributes }
}
}
impl Mountable<Dom> for HtmlViewState {
impl<At> Mountable<Dom> for HtmlViewState<At>
where
At: Attribute<Dom>,
{
fn unmount(&mut self) {}
fn mount(

View file

@ -171,9 +171,9 @@ pub struct ServerMetaContext {
/// Metadata associated with the `<title>` element.
pub(crate) title: TitleContext,
/// Attributes for the `<html>` element.
pub(crate) html: Sender<AnyAttribute<Dom>>,
pub(crate) html: Sender<String>,
/// Attributes for the `<body>` element.
pub(crate) body: Sender<AnyAttribute<Dom>>,
pub(crate) body: Sender<String>,
/// Arbitrary elements to be added to the `<head>` as HTML.
pub(crate) elements: Sender<String>,
}
@ -184,8 +184,8 @@ pub struct ServerMetaContext {
#[derive(Debug)]
pub struct ServerMetaContextOutput {
pub(crate) title: TitleContext,
html: Receiver<AnyAttribute<Dom>>,
body: Receiver<AnyAttribute<Dom>>,
html: Receiver<String>,
body: Receiver<String>,
elements: Receiver<String>,
}
@ -233,13 +233,11 @@ impl ServerMetaContextOutput {
.unwrap_or(0);
// collect all registered meta tags
let meta_buf = self.elements.into_iter().collect::<String>();
let meta_buf = self.elements.try_iter().collect::<String>();
// get HTML strings for `<html>` and `<body>`
let mut html_attrs = String::new();
_ = attributes_to_html(self.html, &mut html_attrs);
let mut body_attrs = String::new();
_ = attributes_to_html(self.body, &mut body_attrs);
let html_attrs = self.html.try_iter().collect::<String>();
let body_attrs = self.body.try_iter().collect::<String>();
let mut modified_chunk = if title_len == 0 && meta_buf.is_empty() {
first_chunk