Skip to content

Commit a603477

Browse files
committed
feat!: impl ssr for simply-typed html components
1 parent 5b45614 commit a603477

File tree

13 files changed

+360
-5
lines changed

13 files changed

+360
-5
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/frender-hook-element/src/element.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ mod prelude_names {
1414
#[cfg(feature = "csr")]
1515
pub(super) use frender_dom::Dom;
1616

17+
#[cfg(feature = "ssr")]
18+
pub(super) use frender_ssr::AnySsrContext;
19+
1720
pub(super) use super::{fn_wrapper, prelude_names, FnHookElement};
1821
pub(super) use crate::{ContextAndState, Rendered};
1922
}
@@ -22,6 +25,7 @@ pub mod new_fn_hook_element {
2225
use super::prelude_names::*;
2326

2427
#[inline]
28+
#[cfg(feature = "csr")]
2529
pub fn csr<HookData: HookPollNextUpdate + HookUnmount + Default, U, E: UpdateRenderState<Dom>>(
2630
use_hook: U,
2731
) -> FnHookElement<HookData, fn_wrapper::FnMutOutputElement<U>, ()>
@@ -33,6 +37,24 @@ pub mod new_fn_hook_element {
3337
_phantom: std::marker::PhantomData,
3438
}
3539
}
40+
41+
#[inline]
42+
#[cfg(feature = "ssr")]
43+
pub fn ssr<
44+
HookData: HookPollNextUpdate + HookUnmount + Default,
45+
U,
46+
E: for<'a> UpdateRenderState<AnySsrContext<'a>>,
47+
>(
48+
use_hook: U,
49+
) -> FnHookElement<HookData, fn_wrapper::FnMutOutputElement<U>, ()>
50+
where
51+
U: FnMut(Pin<&mut HookData>) -> E,
52+
{
53+
FnHookElement {
54+
use_hook: fn_wrapper::FnMutOutputElement(use_hook),
55+
_phantom: std::marker::PhantomData,
56+
}
57+
}
3658
}
3759
#[cfg(all(feature = "csr", feature = "ssr"))]
3860
pub fn new_fn_hook_element() {}

crates/frender-html-components/src/element_macros.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,16 @@ mod simple {
264264
}
265265
}
266266

267+
#[cfg(feature = "ssr")]
268+
mod impl_ssr {
269+
#[allow(unused_imports)]
270+
use super::super::*;
271+
272+
impl<Children> crate::imports::frender_html_simple::IntrinsicComponentSupportChildren<Children> for super::ComponentType {
273+
// TODO: some components are void or self closing
274+
}
275+
}
276+
267277
#[inline(always)]
268278
pub fn build<Children, Props>(
269279
building: Building<Children, Props>,

crates/frender-html-simple/src/component.rs

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1+
#[cfg(feature = "dom")]
12
pub trait IntrinsicComponentWithElementType {
23
type Element;
34
}
45

5-
pub struct IntrinsicComponent<
6-
C: frender_html_common::IntrinsicComponent + IntrinsicComponentWithElementType,
7-
P,
8-
>(pub C, pub P);
6+
#[cfg(feature = "ssr")]
7+
pub trait IntrinsicComponentSupportChildren<Children> {
8+
#[inline]
9+
fn wrap_children(
10+
children: Children,
11+
) -> frender_ssr::element::html::HtmlElementChildren<Children> {
12+
frender_ssr::element::html::HtmlElementChildren::Children(children)
13+
}
14+
}
15+
16+
pub struct IntrinsicComponent<C: frender_html_common::IntrinsicComponent, P>(pub C, pub P);
917

1018
#[cfg(feature = "dom")]
1119
mod dom {
@@ -37,3 +45,52 @@ mod dom {
3745
}
3846
}
3947
}
48+
49+
#[cfg(feature = "ssr")]
50+
mod ssr {
51+
use std::borrow::Cow;
52+
53+
use frender_core::UpdateRenderState;
54+
use frender_html_common::IntrinsicComponent;
55+
use frender_ssr::{AsyncWrite, IntoSsrData, SsrContext};
56+
57+
use super::IntrinsicComponentSupportChildren;
58+
59+
impl<W: AsyncWrite + Unpin, C: IntrinsicComponent, P> UpdateRenderState<SsrContext<W>>
60+
for super::IntrinsicComponent<C, P>
61+
where
62+
C: IntrinsicComponentSupportChildren<P::Children>,
63+
P: IntoSsrData<W>,
64+
{
65+
type State = ::frender_ssr::element::html::HtmlElementRenderState<
66+
'static,
67+
P::ChildrenRenderState,
68+
P::Attrs,
69+
W,
70+
>;
71+
72+
fn initialize_render_state(self, ctx: &mut SsrContext<W>) -> Self::State {
73+
let (children, attributes) = IntoSsrData::into_ssr_data(self.1);
74+
::frender_ssr::element::html::HtmlElement {
75+
tag: Cow::Borrowed(C::INTRINSIC_TAG),
76+
attributes,
77+
children: C::wrap_children(children),
78+
}
79+
.initialize_render_state(ctx)
80+
}
81+
82+
fn update_render_state(
83+
self,
84+
ctx: &mut SsrContext<W>,
85+
state: std::pin::Pin<&mut Self::State>,
86+
) {
87+
let (children, attributes) = IntoSsrData::into_ssr_data(self.1);
88+
::frender_ssr::element::html::HtmlElement {
89+
tag: Cow::Borrowed(C::INTRINSIC_TAG),
90+
attributes,
91+
children: C::wrap_children(children),
92+
}
93+
.update_render_state(ctx, state)
94+
}
95+
}
96+
}

crates/frender-html-simple/src/props.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,27 @@ mod dom {
7575
}
7676
}
7777
}
78+
79+
#[cfg(feature = "ssr")]
80+
mod ssr {
81+
use frender_core::UpdateRenderState;
82+
use frender_ssr::{attrs::IntoIteratorAttrs, AsyncWrite, IntoSsrData, SsrContext};
83+
84+
impl<W: AsyncWrite + Unpin, Children, Props> IntoSsrData<W> for super::ElementProps<Children, Props>
85+
where
86+
Children: UpdateRenderState<SsrContext<W>>,
87+
Children::State: Unpin, // TODO: remove this restriction
88+
Props: IntoIteratorAttrs<'static>,
89+
Props::IntoIterAttrs: Unpin, // TODO: remove this restriction
90+
{
91+
type Children = Children;
92+
93+
type ChildrenRenderState = Children::State;
94+
95+
type Attrs = Props::IntoIterAttrs;
96+
97+
fn into_ssr_data(this: Self) -> (Self::Children, Self::Attrs) {
98+
(this.children, Props::into_iter_attrs(this.other_props))
99+
}
100+
}
101+
}

crates/frender-intrinsic-component-macro/src/simple/field_as_simple_prop.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ impl FieldAsSimpleProp<'_> {
3434
let dom_init;
3535
let dom_update;
3636
let mut ssr_bounds = None;
37+
let ssr_attrs_ty;
38+
let ssr_attrs_impl;
39+
3740
match &field.declaration {
3841
FieldDeclaration::Maybe(m) => {
3942
assert!(m.no_cache.is_none(), "simply typed prop must have state");
@@ -62,6 +65,23 @@ impl FieldAsSimpleProp<'_> {
6265
quote!(this.0),
6366
quote!(&mut state.get_mut().0),
6467
);
68+
69+
ssr_attrs_ty = quote! {
70+
::core::option::IntoIter<
71+
#crate_path::frender_ssr::element::html::HtmlAttrPair<'a>
72+
>
73+
};
74+
75+
let attr_name = m.to_html_prop_name(name);
76+
77+
ssr_attrs_impl = quote! {
78+
V::maybe_into_html_attribute_value(
79+
this.0
80+
).map(|attr_value| (::std::borrow::Cow::Borrowed(#attr_name), attr_value.map_or(
81+
#crate_path::frender_ssr::element::html::HtmlAttributeValue::BooleanTrue,
82+
#crate_path::frender_ssr::element::html::HtmlAttributeValue::String,
83+
))).into_iter()
84+
};
6585
}
6686
FieldDeclaration::EventListener(e) => {
6787
//
@@ -80,6 +100,16 @@ impl FieldAsSimpleProp<'_> {
80100
dom_update = quote! {
81101
V::update_dom_event_listener(this.0, dom_element, &mut state.get_mut().0)
82102
};
103+
104+
ssr_attrs_ty = quote! {
105+
::core::iter::Empty<
106+
#crate_path::frender_ssr::element::html::HtmlAttrPair<'a>
107+
>
108+
};
109+
110+
ssr_attrs_impl = quote! {
111+
::core::iter::empty()
112+
};
83113
}
84114
FieldDeclaration::Full(_) => {
85115
assert_eq!(
@@ -150,7 +180,14 @@ impl FieldAsSimpleProp<'_> {
150180
}
151181
}),
152182
impl_ssr: Some(quote! {
153-
// TODO:
183+
impl<'a, V #ssr_bounds>
184+
#crate_path::frender_ssr::attrs::IntoIteratorAttrs<'a>
185+
for super::props::#name<V> {
186+
type IntoIterAttrs = #ssr_attrs_ty;
187+
fn into_iter_attrs(this: Self) -> Self::IntoIterAttrs {
188+
#ssr_attrs_impl
189+
}
190+
}
154191
}),
155192
field_info: {
156193
let bounds = builder_fn_bounds.map(|b| quote!(: bounds![#b]));

crates/frender-ssr/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@ edition = "2021"
77

88
[dependencies]
99
frender-core = { path = "../frender-core", version = "0.1.0" }
10+
frender-html-common = { version = "0.1.0", path = "../frender-html-common" }
1011
futures-io = "0.3.25"
1112
html-escape = "0.2.13"
13+
pin-project-lite = "0.2.9"
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#![cfg(prototyping)] // TODO: remove
2+
3+
use std::{io, pin::Pin, task::Poll};
4+
5+
use futures_io::AsyncWrite;
6+
7+
pub trait AsyncWritable {
8+
fn poll_write_all<W: AsyncWrite>(
9+
this: Pin<&mut Self>,
10+
writer: Pin<&mut W>,
11+
cx: &mut std::task::Context<'_>,
12+
) -> Poll<io::Result<()>>;
13+
}
14+
15+
pub trait IntoAsyncWritable: Sized {
16+
type AsyncWritable: AsyncWritable;
17+
fn into_async_writable(this: Self) -> Self::AsyncWritable;
18+
}
19+
20+
impl IntoAsyncWritable for () {
21+
type AsyncWritable = Nothing;
22+
23+
fn into_async_writable(_: Self) -> Self::AsyncWritable {
24+
Nothing
25+
}
26+
}
27+
28+
impl<A: IntoAsyncWritable, B: IntoAsyncWritable> IntoAsyncWritable for (A, B) {
29+
type AsyncWritable = Chain<A::AsyncWritable, B::AsyncWritable>;
30+
31+
fn into_async_writable(this: Self) -> Self::AsyncWritable {
32+
Chain {
33+
a: A::into_async_writable(this.0),
34+
b: B::into_async_writable(this.1),
35+
stage: Stage::NoneDone,
36+
}
37+
}
38+
}
39+
40+
enum Stage {
41+
NoneDone,
42+
ADone,
43+
AllDone,
44+
Error,
45+
}
46+
47+
pub struct Nothing;
48+
49+
impl AsyncWritable for Nothing {
50+
fn poll_write_all<W: AsyncWrite>(
51+
_: Pin<&mut Self>,
52+
_: Pin<&mut W>,
53+
_: &mut std::task::Context<'_>,
54+
) -> Poll<io::Result<()>> {
55+
Poll::Ready(Ok(()))
56+
}
57+
}
58+
59+
pin_project_lite::pin_project!(
60+
pub struct Chain<A, B> {
61+
#[pin]
62+
a: A,
63+
#[pin]
64+
b: B,
65+
stage: Stage,
66+
}
67+
);
68+
69+
impl<A, B> Chain<A, B> {}
70+
71+
impl<A: AsyncWritable, B: AsyncWritable> AsyncWritable for Chain<A, B> {
72+
fn poll_write_all<W: AsyncWrite>(
73+
this: Pin<&mut Self>,
74+
mut writer: Pin<&mut W>,
75+
cx: &mut std::task::Context<'_>,
76+
) -> Poll<io::Result<()>> {
77+
let this = this.project();
78+
79+
macro_rules! continue_with {
80+
($e:expr, $next_stage:expr) => {
81+
match $e {
82+
Poll::Ready(Ok(())) => {
83+
*this.stage = $next_stage;
84+
}
85+
err @ Poll::Ready(Err(_)) => {
86+
*this.stage = Stage::Error;
87+
return err;
88+
}
89+
Poll::Pending => return Poll::Pending,
90+
}
91+
};
92+
}
93+
94+
if let Stage::Error = this.stage {
95+
return Poll::Ready(Err(io::ErrorKind::Other.into()));
96+
}
97+
98+
if let Stage::NoneDone = this.stage {
99+
continue_with!(A::poll_write_all(this.a, writer.as_mut(), cx), Stage::ADone)
100+
}
101+
102+
if let Stage::ADone = this.stage {
103+
continue_with!(
104+
B::poll_write_all(this.b, writer.as_mut(), cx),
105+
Stage::AllDone
106+
)
107+
}
108+
109+
assert!(matches!(this.stage, Stage::AllDone));
110+
Poll::Ready(Ok(()))
111+
}
112+
}
113+
114+
mod chain_macro {
115+
macro_rules! Chain {
116+
($ty1:ty $(,)?) => {
117+
$ty1
118+
};
119+
($ty1:ty , $($ty:ty),+ $(,)?) => {
120+
$crate::async_writable::Chain::<
121+
$ty1,
122+
$crate::async_writable::Chain![$($ty),+]
123+
>
124+
};
125+
}
126+
pub(crate) use Chain;
127+
}
128+
129+
pub(crate) use chain_macro::*;

0 commit comments

Comments
 (0)