mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 14:54:16 +00:00
feat: correct HTML rendering for spread attributes on <Body/> and <Html/>
This commit is contained in:
parent
e9c7b50dfd
commit
f7b16b726b
3 changed files with 114 additions and 143 deletions
120
meta/src/body.rs
120
meta/src/body.rs
|
@ -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 document’s `<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(
|
||||
|
|
121
meta/src/html.rs
121
meta/src/html.rs
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue