feat: ErrorBoundary and Suspense

This commit is contained in:
Greg Johnston 2024-04-03 21:19:40 -04:00
parent d7c62622ae
commit c06110128b
26 changed files with 464 additions and 94 deletions

View file

@ -1,27 +1,29 @@
use leptos::{component, create_signal, prelude::*, view, IntoView};
use leptos::{component, prelude::*, signal, view, ErrorBoundary, IntoView};
#[component]
pub fn App() -> impl IntoView {
let (value, set_value) = create_signal(Ok(0)); //"".parse::<i32>());
let (value, set_value) = signal("".parse::<i32>());
let guard = value.read();
view! {
<h1>"Error Handling"</h1>
<label>
"Type a number (or something that's not a number!)"
<input
type="text"
value=move || value.get().map(|n| n.to_string()).unwrap_or_default()// TODO guard support here
// when input changes, try to parse a number from the input
on:input:target=move |ev| set_value.set(ev.target().value().parse::<i32>())
/>
<h1>"Error Handling"</h1>
<label>
"Type a number (or something that's not a number!)"
<input
type="text"
value=move || value.get().map(|n| n.to_string()).unwrap_or_default()// TODO guard support here
// when input changes, try to parse a number from the input
on:input:target=move |ev| set_value.set(ev.target().value().parse::<i32>())
/>
// If an `Err(_) had been rendered inside the <ErrorBoundary/>,
// the fallback will be displayed. Otherwise, the children of the
// <ErrorBoundary/> will be displayed.
/* <ErrorBoundary
// the fallback receives a signal containing current errors
fallback=|errors| view! {
// If an `Err(_) had been rendered inside the <ErrorBoundary/>,
// the fallback will be displayed. Otherwise, the children of the
// <ErrorBoundary/> will be displayed.
<ErrorBoundary
// the fallback receives a signal containing current errors
fallback=|errors| {
let errors = errors.clone();
view! {
<div class="error">
<p>"Not a number! Errors: "</p>
// we can render a list of errors
@ -30,31 +32,23 @@ pub fn App() -> impl IntoView {
{move || errors.get()
.into_iter()
.map(|(_, e)| view! { <li>{e.to_string()}</li>})
.collect_view()
.collect::<Vec<_>>()
}
</ul>
</div>
}
>*/
{move || view! {
<p>
"You entered "
// because `value` is `Result<i32, _>`,
// it will render the `i32` if it is `Ok`,
// and render nothing and trigger the error boundary
// if it is `Err`. It's a signal, so this will dynamically
// update when `value` changes
{move || value.get()}
//<strong>{move || value.get()}</strong>
</p>}
.catch(|e| view! {
<p class="error">{e.to_string()}</p>
})
}
//</ErrorBoundary>
</label>
}
}
>
<p>
"You entered "
// because `value` is `Result<i32, _>`,
// it will render the `i32` if it is `Ok`,
// and render nothing and trigger the error boundary
// if it is `Err`. It's a signal, so this will dynamically
// update when `value` changes
<strong>{move || value.get()}</strong> // TODO render signal directly
</p>
</ErrorBoundary>
</label>
}
}
#[component]
pub fn ErrorBoundary() -> impl IntoView {}

View file

@ -5,7 +5,7 @@ use leptos::{
computed::AsyncDerived,
signal::{signal, ArcRwSignal},
},
view, ErrorBoundary, Errors, IntoView, Transition,
view, ErrorBoundary, Errors, IntoView, Suspense, Transition,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
@ -25,6 +25,7 @@ type CatCount = usize;
async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
if count > 0 {
gloo_timers::future::TimeoutFuture::new(1000).await;
// make the request
let res = reqwasm::http::Request::get(&format!(
"https://api.thecatapi.com/v1/images/search?limit={count}",
@ -76,14 +77,17 @@ pub fn fetch_example() -> impl IntoView {
// TODO weaving together Transition and ErrorBoundary is hard with the new async API for
// suspense, because Transition expects a Future as its children, and ErrorBoundary isn't a
// future
let cats_view = move || async move {
cats.await.map(|cats| {
cats.into_iter()
.map(|s| view! { <p><img src={s}/></p> })
.collect::<Vec<_>>()
})
//.catch(|e| view! { <p class="error">{e.to_string()}</p> })
};
/*let cats_view = move || {
async move {
cats.await.map(|cats| {
cats.into_iter()
.map(|s| view! { <p><img src={s}/></p> })
.collect::<Vec<_>>()
})
//.catch(|e| view! { <p class="error">{e.to_string()}</p> })
}
.suspend()
};*/
view! {
<div>
@ -98,11 +102,22 @@ pub fn fetch_example() -> impl IntoView {
}
/>
</label>
<ErrorBoundary fallback>
<Transition fallback=|| view! { <div>"Loading..."</div> }>
{cats_view()}
</Transition>
</ErrorBoundary>
<ErrorBoundary fallback>
<Suspense fallback=|| view! { <div>"Loading..."</div> }>
<ul>
{
async move {
cats.await.map(|cats| {
cats.into_iter()
.map(|s| view! { <li><img src={s}/></li> })
.collect::<Vec<_>>()
})
}
.suspend()
}
</ul>
</Suspense>
</ErrorBoundary>
</div>
}
}

View file

@ -222,3 +222,30 @@ where
TypedChildrenMut(Box::new(move || f().into_view()))
}
}
/// A typed equivalent to [`ChildrenFn`], which takes a generic but preserves type information to
/// allow the compiler to optimize the view more effectively.
pub struct TypedChildrenFn<T>(Arc<dyn Fn() -> View<T> + Send + Sync>);
impl<T> Debug for TypedChildrenFn<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("TypedChildrenFn").finish()
}
}
impl<T> TypedChildrenFn<T> {
pub fn into_inner(self) -> Arc<dyn Fn() -> View<T> + Send + Sync> {
self.0
}
}
impl<F, C> ToChildren<F> for TypedChildrenFn<C>
where
F: Fn() -> C + Send + Sync + 'static,
C: IntoView,
{
#[inline]
fn to_children(f: F) -> Self {
TypedChildrenFn(Arc::new(move || f().into_view()))
}
}

View file

@ -93,6 +93,7 @@ where
{
type State = ErrorBoundaryViewState<Chil, Fal, Rndr>;
type FallibleState = ();
type AsyncOutput = ErrorBoundaryView<Chil, FalFn, Fal, Rndr>;
fn build(self) -> Self::State {
let Self {
@ -169,6 +170,10 @@ where
) -> any_error::Result<()> {
todo!()
}
async fn resolve(self) -> Self::AsyncOutput {
self
}
}
impl<Chil, FalFn, Fal, Rndr> RenderHtml<Rndr>
@ -227,7 +232,7 @@ where
Either::Left(chil) => chil.unmount(),
Either::Right((fal, _)) => fal.unmount(),
});
self.placeholder.unmount();
//self.placeholder.unmount();
}
fn mount(

View file

@ -38,6 +38,7 @@ where
impl<T: Render<Dom>> Render<Dom> for View<T> {
type State = T::State;
type FallibleState = T::FallibleState;
type AsyncOutput = T::AsyncOutput;
fn build(self) -> Self::State {
self.0.build()
@ -57,6 +58,10 @@ impl<T: Render<Dom>> Render<Dom> for View<T> {
) -> any_error::Result<()> {
self.0.try_rebuild(state)
}
async fn resolve(self) -> Self::AsyncOutput {
self.0.resolve().await
}
}
impl<T: RenderHtml<Dom>> RenderHtml<Dom> for View<T> {

View file

@ -1,10 +1,11 @@
use crate::{
children::{ToChildren, ViewFn},
children::{ToChildren, TypedChildrenFn, TypedChildrenMut, ViewFn},
IntoView,
};
use leptos_macro::component;
use leptos_reactive::untrack;
use std::{future::Future, sync::Arc};
use tachys::prelude::FutureViewExt;
use tachys::{async_views::SuspenseBoundary, prelude::FutureViewExt};
/// An async, typed equivalent to [`Children`], which takes a generic but preserves
/// type information to allow the compiler to optimize the view more effectively.
@ -35,21 +36,24 @@ where
/// TODO docs!
#[component]
pub fn Suspense<Chil, ChilFn, ChilFut>(
pub fn Suspense<Chil>(
#[prop(optional, into)] fallback: ViewFn,
children: AsyncChildren<Chil, ChilFn, ChilFut>,
children: TypedChildrenFn<Chil>,
) -> impl IntoView
where
Chil: IntoView + 'static,
ChilFn: Fn() -> ChilFut + Send + Clone + 'static,
ChilFut: Future<Output = Chil> + Send + 'static,
{
let children = children.into_inner();
let fallback = move || fallback.clone().run();
// TODO check this against islands
move || {
(children.clone())()
.suspend()
.with_fallback(fallback.run())
.track()
crate::logging::log!("running innner thing");
untrack(|| {
SuspenseBoundary::<false, _, _>::new(
fallback.clone(),
(children.clone())(),
)
})
// TODO track
}
}

View file

@ -17,7 +17,7 @@ async fn arc_async_derived_calculates_eagerly() {
42
});
assert_eq!(*value.clone().await, 42);
assert_eq!(value.clone().await, 42);
std::mem::forget(value);
}
@ -34,13 +34,13 @@ async fn arc_async_derived_tracks_signal_change() {
signal.get()
});
assert_eq!(*value.clone().await, 10);
assert_eq!(value.clone().await, 10);
signal.set(30);
sleep(Duration::from_millis(5)).await;
assert_eq!(*value.clone().await, 30);
assert_eq!(value.clone().await, 30);
signal.set(50);
sleep(Duration::from_millis(5)).await;
assert_eq!(*value.clone().await, 50);
assert_eq!(value.clone().await, 50);
std::mem::forget(value);
}
@ -56,7 +56,7 @@ async fn async_derived_calculates_eagerly() {
42
});
assert_eq!(*value.await, 42);
assert_eq!(value.await, 42);
}
#[tokio::test]

View file

@ -11,7 +11,139 @@ use any_spawner::Executor;
use either_of::Either;
use futures::FutureExt;
use parking_lot::RwLock;
use std::{fmt::Debug, future::Future, sync::Arc};
use std::{cell::RefCell, fmt::Debug, future::Future, rc::Rc, sync::Arc};
pub struct SuspenseBoundary<const TRANSITION: bool, FalFn, Chil> {
fallback: FalFn,
children: Chil,
}
impl<const TRANSITION: bool, FalFn, Chil>
SuspenseBoundary<TRANSITION, FalFn, Chil>
{
pub fn new(fallback: FalFn, children: Chil) -> Self {
Self { fallback, children }
}
}
impl<const TRANSITION: bool, FalFn, Fal, Chil, Rndr> Render<Rndr>
for SuspenseBoundary<TRANSITION, FalFn, Chil>
where
FalFn: Fn() -> Fal,
Fal: Render<Rndr> + 'static,
Chil: Render<Rndr> + 'static,
Chil::State: 'static,
Rndr: Renderer + 'static,
{
type State = Rc<
RefCell<
EitherState<
Fal::State,
<Chil::AsyncOutput as Render<Rndr>>::State,
Rndr,
>,
>,
>;
type FallibleState = ();
type AsyncOutput = Self;
fn build(self) -> Self::State {
let fut = self.children.resolve();
#[cfg(feature = "reactive_graph")]
let fut = reactive_graph::computed::ScopedFuture::new(fut);
let initial = Either::<
Fal::State,
<Chil::AsyncOutput as Render<Rndr>>::State,
>::Left((self.fallback)().build());
// now we can build the initial state
let marker = Rndr::create_placeholder();
let state = Rc::new(RefCell::new(EitherState {
state: initial,
marker: marker.clone(),
}));
// if the initial state was pending, spawn a future to wait for it
// spawning immediately means that our now_or_never poll result isn't lost
// if it wasn't pending at first, we don't need to poll the Future again
Executor::spawn_local({
let state = Rc::clone(&state);
let marker = marker.clone();
async move {
let mut value = fut.await;
let mut state = state.borrow_mut();
Either::<Fal, Chil::AsyncOutput>::Right(value)
.rebuild(&mut *state);
}
});
state
}
fn rebuild(self, state: &mut Self::State) {
if !TRANSITION {
Either::<Fal, Chil::AsyncOutput>::Left((self.fallback)())
.rebuild(&mut *state.borrow_mut());
}
// spawn the future, and rebuild the state when it resolves
let fut = self.children.resolve();
#[cfg(feature = "reactive_graph")]
let fut = reactive_graph::computed::ScopedFuture::new(fut);
Executor::spawn_local({
let state = Rc::clone(state);
async move {
let value = fut.await;
let mut state = state.borrow_mut();
let fut = Either::<Fal, Chil::AsyncOutput>::Right(value)
.rebuild(&mut *state);
}
});
}
fn try_build(self) -> any_error::Result<Self::FallibleState> {
todo!()
}
fn try_rebuild(
self,
state: &mut Self::FallibleState,
) -> any_error::Result<()> {
todo!()
}
// building/rebuild SuspenseBoundary asynchronously just runs the Suspense:
// i.e., if you nest a SuspenseBoundary inside another SuspenseBoundary, the parent will not
// wait for the child to load
async fn resolve(self) -> Self::AsyncOutput {
self
}
}
impl<const TRANSITION: bool, FalFn, Fal, Chil, Rndr> RenderHtml<Rndr>
for SuspenseBoundary<TRANSITION, FalFn, Chil>
where
FalFn: Fn() -> Fal,
Fal: RenderHtml<Rndr> + 'static,
Chil: RenderHtml<Rndr> + 'static,
Chil::State: 'static,
Rndr: Renderer + 'static,
{
const MIN_LENGTH: usize = 0; // TODO
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
todo!()
}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &Cursor<Rndr>,
position: &PositionState,
) -> Self::State {
todo!()
}
}
pub trait FutureViewExt: Sized {
fn suspend(self) -> Suspend<false, (), Self>
@ -71,6 +203,7 @@ where
>;
// TODO fallible state/error
type FallibleState = Self::State;
type AsyncOutput = Fut::Output;
fn build(self) -> Self::State {
// poll the future once immediately
@ -134,6 +267,10 @@ where
) -> any_error::Result<()> {
todo!()
}
async fn resolve(self) -> Self::AsyncOutput {
self.fut.await
}
}
impl<const TRANSITION: bool, Fal, Fut, Rndr> RenderHtml<Rndr>
@ -267,3 +404,31 @@ where
self.write().insert_before_this(parent, child)
}
}
impl<Rndr, Fal, Output> Mountable<Rndr>
for Rc<RefCell<EitherState<Fal, Output, Rndr>>>
where
Fal: Mountable<Rndr>,
Output: Mountable<Rndr>,
Rndr: Renderer,
{
fn unmount(&mut self) {
self.borrow_mut().unmount();
}
fn mount(
&mut self,
parent: &<Rndr as Renderer>::Element,
marker: Option<&<Rndr as Renderer>::Node>,
) {
self.borrow_mut().mount(parent, marker);
}
fn insert_before_this(
&self,
parent: &<Rndr as Renderer>::Element,
child: &mut dyn Mountable<Rndr>,
) -> bool {
self.borrow_mut().insert_before_this(parent, child)
}
}

View file

@ -167,6 +167,7 @@ where
{
type State = ElementState<At::State, Ch::State, Rndr>;
type FallibleState = ElementState<At::State, Ch::FallibleState, Rndr>;
type AsyncOutput = HtmlElement<E, At, Ch::AsyncOutput, Rndr>;
fn rebuild(self, state: &mut Self::State) {
let ElementState {
@ -213,6 +214,16 @@ where
self.children.try_rebuild(children)?;
Ok(())
}
async fn resolve(self) -> Self::AsyncOutput {
HtmlElement {
tag: self.tag,
// TODO async attributes too
attributes: self.attributes,
children: self.children.resolve().await,
rndr: PhantomData,
}
}
}
impl<E, At, Ch, Rndr> RenderHtml<Rndr> for HtmlElement<E, At, Ch, Rndr>

View file

@ -49,6 +49,7 @@ where
{
type State = View::State;
type FallibleState = View::FallibleState;
type AsyncOutput = View::AsyncOutput;
fn build(self) -> Self::State {
self.view.build()
@ -68,6 +69,10 @@ where
) -> any_error::Result<()> {
self.view.try_rebuild(state)
}
async fn resolve(self) -> Self::AsyncOutput {
self.view.resolve().await
}
}
impl<Rndr, View> RenderHtml<Rndr> for Island<Rndr, View>
@ -152,6 +157,7 @@ where
{
type State = ();
type FallibleState = Self::State;
type AsyncOutput = View::AsyncOutput;
fn build(self) -> Self::State {}
@ -167,6 +173,11 @@ where
) -> any_error::Result<()> {
Ok(())
}
async fn resolve(self) -> Self::AsyncOutput {
// TODO should this be wrapped?
self.view.resolve().await
}
}
impl<Rndr, View> RenderHtml<Rndr> for IslandChildren<Rndr, View>

View file

@ -28,6 +28,7 @@ pub fn doctype<R: Renderer>(value: &'static str) -> Doctype<R> {
impl<R: Renderer> Render<R> for Doctype<R> {
type State = ();
type FallibleState = Self::State;
type AsyncOutput = Self;
fn build(self) -> Self::State {}
@ -43,6 +44,10 @@ impl<R: Renderer> Render<R> for Doctype<R> {
) -> any_error::Result<()> {
Ok(())
}
async fn resolve(self) -> Self::AsyncOutput {
self
}
}
impl<R> RenderHtml<R> for Doctype<R>

View file

@ -15,6 +15,7 @@ pub struct OcoStrState<R: Renderer> {
impl<R: Renderer> Render<R> for Oco<'static, str> {
type State = OcoStrState<R>;
type FallibleState = Self::State;
type AsyncOutput = Self;
fn build(self) -> Self::State {
let node = R::create_text_node(&self);
@ -40,6 +41,10 @@ impl<R: Renderer> Render<R> for Oco<'static, str> {
<Self as Render<R>>::rebuild(self, state);
Ok(())
}
async fn resolve(self) -> Self::AsyncOutput {
self
}
}
impl<R> RenderHtml<R> for Oco<'static, str>

View file

@ -52,12 +52,12 @@ macro_rules! render_primitive {
}
}
impl<'a, G, R: Renderer> Render<R> for ReadGuard<$child_type, G>
impl<G, R: Renderer> Render<R> for ReadGuard<$child_type, G>
where G: Deref<Target = $child_type>
{
type State = [<ReadGuard $child_type:camel State>]<R>;
type FallibleState = Self::State;
type AsyncOutput = Self;
fn build(self) -> Self::State {
let node = R::create_text_node(&self.to_string());
@ -79,6 +79,10 @@ macro_rules! render_primitive {
fn try_rebuild(self, state: &mut Self::FallibleState) -> any_error::Result<()> {
self.rebuild(state);
Ok(())
}
async fn resolve(self) -> Self::AsyncOutput {
self
}
}
@ -206,6 +210,7 @@ where
{
type State = ReadGuardStringState<R>;
type FallibleState = Self::State;
type AsyncOutput = Self;
fn build(self) -> Self::State {
let node = R::create_text_node(&self);
@ -235,6 +240,10 @@ where
self.rebuild(state);
Ok(())
}
async fn resolve(self) -> Self::AsyncOutput {
self
}
}
impl<G, R> RenderHtml<R> for ReadGuard<String, G>

View file

@ -57,6 +57,8 @@ where
type State = RenderEffectState<V::State>;
type FallibleState =
RenderEffectState<Result<V::FallibleState, Option<AnyError>>>;
// TODO how this should be handled?
type AsyncOutput = Self;
#[track_caller]
fn build(mut self) -> Self::State {
@ -136,7 +138,6 @@ where
self,
state: &mut Self::FallibleState,
) -> any_error::Result<()> {
crate::log("RenderEffect::try_rebuild");
if let Some(inner) = &mut state.0 {
inner
.with_value_mut(|value| match value {
@ -148,8 +149,11 @@ where
Ok(())
}
}
}
async fn resolve(self) -> Self::AsyncOutput {
self
}
}
pub struct RenderEffectState<T: 'static>(Option<RenderEffect<T>>);
impl<T> From<RenderEffect<T>> for RenderEffectState<T> {

View file

@ -69,6 +69,7 @@ where
{
type State = OwnedViewState<T::State, R>;
type FallibleState = OwnedViewState<T::FallibleState, R>;
type AsyncOutput = OwnedView<T::AsyncOutput, R>;
fn build(self) -> Self::State {
let state = self.owner.with(|| self.view.build());
@ -91,6 +92,10 @@ where
) -> any_error::Result<()> {
todo!()
}
async fn resolve(self) -> Self::AsyncOutput {
todo!()
}
}
impl<T, R> RenderHtml<R> for OwnedView<T, R>

View file

@ -261,6 +261,7 @@ where
{
type State = AnyViewState<R>;
type FallibleState = Self::State;
type AsyncOutput = Self;
fn build(self) -> Self::State {
(self.build)(self.value)
@ -280,6 +281,11 @@ where
) -> any_error::Result<()> {
todo!()
}
async fn resolve(self) -> Self::AsyncOutput {
// we probably do need a function for this
todo!()
}
}
impl<R> RenderHtml<R> for AnyView<R>

View file

@ -17,7 +17,7 @@ where
Rndr: Renderer,
{
pub state: Either<A, B>,
marker: Rndr::Placeholder,
pub marker: Rndr::Placeholder,
}
impl<A, B, Rndr> Render<Rndr> for Either<A, B>
@ -28,6 +28,7 @@ where
{
type State = EitherState<A::State, B::State, Rndr>;
type FallibleState = EitherState<A::FallibleState, B::FallibleState, Rndr>;
type AsyncOutput = Either<A::AsyncOutput, B::AsyncOutput>;
fn build(self) -> Self::State {
let marker = Rndr::create_placeholder();
@ -73,6 +74,13 @@ where
) -> any_error::Result<()> {
todo!()
}
async fn resolve(self) -> Self::AsyncOutput {
match self {
Either::Left(left) => Either::Left(left.resolve().await),
Either::Right(right) => Either::Right(right.resolve().await),
}
}
}
impl<A, B, Rndr> Mountable<Rndr> for EitherState<A, B, Rndr>
@ -248,7 +256,7 @@ macro_rules! tuples {
{
type State = [<EitherOf $num State>]<$($ty,)* Rndr>;
type FallibleState = [<EitherOf $num State>]<$($ty,)* Rndr>;
type AsyncOutput = [<EitherOf $num>]<$($ty::AsyncOutput,)*>;
fn build(self) -> Self::State {
let marker = Rndr::create_placeholder();
@ -290,6 +298,12 @@ macro_rules! tuples {
) -> any_error::Result<()> {
todo!()
}
async fn resolve(self) -> Self::AsyncOutput {
match self {
$([<EitherOf $num>]::$ty(this) => [<EitherOf $num>]::$ty(this.resolve().await),)*
}
}
}
impl<Rndr, $($ty,)*> RenderHtml<Rndr> for [<EitherOf $num>]<$($ty,)*>

View file

@ -16,6 +16,7 @@ where
{
type State = ResultState<T::State, R>;
type FallibleState = T::State;
type AsyncOutput = Result<T::AsyncOutput, E>;
fn build(self) -> Self::State {
let placeholder = R::create_placeholder();
@ -61,6 +62,13 @@ where
) -> any_error::Result<()> {
todo!()
}
async fn resolve(self) -> Self::AsyncOutput {
match self {
Ok(view) => Ok(view.resolve().await),
Err(e) => Err(e),
}
}
}
/// View state for a `Result<_, _>` view.
@ -234,6 +242,7 @@ where
{
type State = TryState<T, Fal, Rndr>;
type FallibleState = Self::State;
type AsyncOutput = Try<T::AsyncOutput, Fal, FalFn, Rndr>;
fn build(mut self) -> Self::State {
let inner = match self.child.try_build() {
@ -309,6 +318,10 @@ where
self.rebuild(state);
Ok(())
}
async fn resolve(self) -> Self::AsyncOutput {
todo!()
}
}
// TODO RenderHtml implementation for ErrorBoundary

View file

@ -13,6 +13,7 @@ where
{
type State = OptionState<T::State, R>;
type FallibleState = OptionState<T::FallibleState, R>;
type AsyncOutput = Option<T::AsyncOutput>;
fn build(self) -> Self::State {
let placeholder = R::create_placeholder();
@ -71,6 +72,13 @@ where
) -> any_error::Result<()> {
todo!()
}
async fn resolve(self) -> Self::AsyncOutput {
match self {
None => None,
Some(value) => Some(value.resolve().await),
}
}
}
impl<T, R> RenderHtml<R> for Option<T>
@ -189,6 +197,7 @@ where
{
type State = VecState<T::State, R>;
type FallibleState = VecState<T::FallibleState, R>;
type AsyncOutput = Vec<T::AsyncOutput>;
fn build(self) -> Self::State {
VecState {
@ -264,6 +273,13 @@ where
) -> any_error::Result<()> {
todo!()
}
async fn resolve(self) -> Self::AsyncOutput {
futures::future::join_all(self.into_iter().map(T::resolve))
.await
.into_iter()
.collect::<Vec<_>>()
}
}
pub struct VecState<T, R>

View file

@ -74,6 +74,7 @@ where
type State = KeyedState<K, V, Rndr>;
// TODO fallible state and try_build()/try_rebuild() here
type FallibleState = Self::State;
type AsyncOutput = Self;
fn build(self) -> Self::State {
let items = self.items.into_iter();
@ -138,6 +139,10 @@ where
) -> any_error::Result<()> {
todo!()
}
async fn resolve(self) -> Self::AsyncOutput {
todo!()
}
}
impl<T, I, K, KF, VF, V, Rndr> RenderHtml<Rndr>

View file

@ -1,6 +1,6 @@
use crate::{hydration::Cursor, renderer::Renderer, ssr::StreamBuilder};
use parking_lot::RwLock;
use std::sync::Arc;
use std::{future::Future, sync::Arc};
pub mod add_attr;
pub mod any_view;
@ -26,6 +26,7 @@ pub trait Render<R: Renderer>: Sized {
/// and the previous string, to allow for diffing between updates.
type State: Mountable<R>;
type FallibleState: Mountable<R>;
type AsyncOutput: Render<R>;
/// Creates the view for the first time, without hydrating from existing HTML.
fn build(self) -> Self::State;
@ -39,6 +40,8 @@ pub trait Render<R: Renderer>: Sized {
self,
state: &mut Self::FallibleState,
) -> any_error::Result<()>;
fn resolve(self) -> impl Future<Output = Self::AsyncOutput>;
}
#[derive(Debug, Clone, Copy)]

View file

@ -21,7 +21,7 @@ macro_rules! render_primitive {
paste::paste! {
pub struct [<$child_type:camel State>]<R>(R::Text, $child_type) where R: Renderer;
impl<'a, R: Renderer> Mountable<R> for [<$child_type:camel State>]<R> {
impl<R: Renderer> Mountable<R> for [<$child_type:camel State>]<R> {
fn unmount(&mut self) {
self.0.unmount()
}
@ -44,10 +44,10 @@ macro_rules! render_primitive {
}
}
impl<'a, R: Renderer> Render<R> for $child_type {
impl<R: Renderer> Render<R> for $child_type {
type State = [<$child_type:camel State>]<R>;
type FallibleState = Self::State;
type AsyncOutput = Self;
fn build(self) -> Self::State {
let node = R::create_text_node(&self.to_string());
@ -68,11 +68,15 @@ macro_rules! render_primitive {
fn try_rebuild(self, state: &mut Self::FallibleState) -> any_error::Result<()> {
self.rebuild(state);
Ok(())
Ok(())
}
async fn resolve(self) -> Self::AsyncOutput {
self
}
}
impl<'a, R> RenderHtml<R> for $child_type
impl<R> RenderHtml<R> for $child_type
where
R: Renderer,

View file

@ -130,6 +130,7 @@ where
{
type State = Option<R::Text>;
type FallibleState = Self::State;
type AsyncOutput = Self;
fn build(self) -> Self::State {
// a view state has to be returned so it can be mounted
@ -150,6 +151,10 @@ where
Render::<R>::rebuild(self, state);
Ok(())
}
fn resolve(self) -> futures::future::Ready<Self::AsyncOutput> {
futures::future::ready(self)
}
}
impl<const V: &'static str, R> RenderHtml<R> for Static<V>

View file

@ -15,6 +15,7 @@ pub struct StrState<'a, R: Renderer> {
impl<'a, R: Renderer> Render<R> for &'a str {
type State = StrState<'a, R>;
type FallibleState = Self::State;
type AsyncOutput = Self;
fn build(self) -> Self::State {
let node = R::create_text_node(self);
@ -40,6 +41,10 @@ impl<'a, R: Renderer> Render<R> for &'a str {
self.rebuild(state);
Ok(())
}
async fn resolve(self) -> Self::AsyncOutput {
self
}
}
impl<'a, R> RenderHtml<R> for &'a str
@ -142,6 +147,7 @@ pub struct StringState<R: Renderer> {
impl<R: Renderer> Render<R> for String {
type State = StringState<R>;
type FallibleState = Self::State;
type AsyncOutput = Self;
fn build(self) -> Self::State {
let node = R::create_text_node(&self);
@ -167,6 +173,10 @@ impl<R: Renderer> Render<R> for String {
self.rebuild(state);
Ok(())
}
async fn resolve(self) -> Self::AsyncOutput {
self
}
}
impl<R> RenderHtml<R> for String
@ -241,6 +251,7 @@ pub struct RcStrState<R: Renderer> {
impl<R: Renderer> Render<R> for Rc<str> {
type State = RcStrState<R>;
type FallibleState = Self::State;
type AsyncOutput = Self;
fn build(self) -> Self::State {
let node = R::create_text_node(&self);
@ -266,6 +277,10 @@ impl<R: Renderer> Render<R> for Rc<str> {
self.rebuild(state);
Ok(())
}
async fn resolve(self) -> Self::AsyncOutput {
self
}
}
impl<R> RenderHtml<R> for Rc<str>
@ -341,6 +356,7 @@ pub struct ArcStrState<R: Renderer> {
impl<R: Renderer> Render<R> for Arc<str> {
type State = ArcStrState<R>;
type FallibleState = Self::State;
type AsyncOutput = Self;
fn build(self) -> Self::State {
let node = R::create_text_node(&self);
@ -366,6 +382,10 @@ impl<R: Renderer> Render<R> for Arc<str> {
self.rebuild(state);
Ok(())
}
async fn resolve(self) -> Self::AsyncOutput {
self
}
}
impl<R> RenderHtml<R> for Arc<str>
@ -441,6 +461,7 @@ pub struct CowStrState<'a, R: Renderer> {
impl<'a, R: Renderer> Render<R> for Cow<'a, str> {
type State = CowStrState<'a, R>;
type FallibleState = Self::State;
type AsyncOutput = Self;
fn build(self) -> Self::State {
let node = R::create_text_node(&self);
@ -466,6 +487,10 @@ impl<'a, R: Renderer> Render<R> for Cow<'a, str> {
self.rebuild(state);
Ok(())
}
async fn resolve(self) -> Self::AsyncOutput {
self
}
}
impl<'a, R> RenderHtml<R> for Cow<'a, str>

View file

@ -1,16 +1,8 @@
use super::{
Mountable, Position, PositionState, Render, RenderHtml, ToTemplate,
};
use crate::{
dom::document,
hydration::Cursor,
renderer::{dom::Dom, DomRenderer},
};
use linear_map::LinearMap;
use once_cell::unsync::Lazy;
use std::{any::TypeId, cell::RefCell, marker::PhantomData};
use wasm_bindgen::JsCast;
use web_sys::HtmlTemplateElement;
use crate::{hydration::Cursor, renderer::DomRenderer};
use std::marker::PhantomData;
pub struct ViewTemplate<V, R>
where
@ -46,6 +38,7 @@ where
{
type State = V::State;
type FallibleState = V::FallibleState;
type AsyncOutput = Self;
// TODO try_build/try_rebuild()
@ -70,6 +63,10 @@ where
) -> any_error::Result<()> {
todo!()
}
async fn resolve(self) -> Self::AsyncOutput {
self
}
}
impl<V, R> RenderHtml<R> for ViewTemplate<V, R>

View file

@ -14,6 +14,7 @@ use const_str_slice_concat::{
impl<R: Renderer> Render<R> for () {
type State = ();
type FallibleState = Self::State;
type AsyncOutput = ();
fn build(self) -> Self::State {}
@ -29,6 +30,8 @@ impl<R: Renderer> Render<R> for () {
) -> any_error::Result<()> {
Ok(())
}
async fn resolve(self) -> Self::AsyncOutput {}
}
impl<R> RenderHtml<R> for ()
@ -102,6 +105,7 @@ impl ToTemplate for () {
impl<A: Render<R>, R: Renderer> Render<R> for (A,) {
type State = A::State;
type FallibleState = A::FallibleState;
type AsyncOutput = (A::AsyncOutput,);
fn build(self) -> Self::State {
self.0.build()
@ -121,6 +125,10 @@ impl<A: Render<R>, R: Renderer> Render<R> for (A,) {
) -> any_error::Result<()> {
self.0.try_rebuild(state)
}
async fn resolve(self) -> Self::AsyncOutput {
(self.0.resolve().await,)
}
}
impl<A, R> RenderHtml<R> for (A,)
@ -210,8 +218,8 @@ macro_rules! impl_view_for_tuples {
Rndr: Renderer
{
type State = ($first::State, $($ty::State,)*);
type FallibleState = ($first::FallibleState, $($ty::FallibleState,)*);
type AsyncOutput = ($first::AsyncOutput, $($ty::AsyncOutput,)*);
fn build(self) -> Self::State {
#[allow(non_snake_case)]
@ -249,6 +257,15 @@ macro_rules! impl_view_for_tuples {
}
Ok(())
}
async fn resolve(self) -> Self::AsyncOutput {
#[allow(non_snake_case)]
let ($first, $($ty,)*) = self;
futures::join!(
$first.resolve(),
$($ty.resolve()),*
)
}
}
impl<$first, $($ty),*, Rndr> RenderHtml<Rndr> for ($first, $($ty,)*)