diff --git a/Cargo.toml b/Cargo.toml index 638424a167d..862adb4c4ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ members = [ "examples/two_apps", "examples/webgl", "examples/web_worker_fib", + "examples/suspense", # Tools "tools/changelog", diff --git a/examples/function_todomvc/src/components/entry.rs b/examples/function_todomvc/src/components/entry.rs index d55c477f15f..e59b3ac742b 100644 --- a/examples/function_todomvc/src/components/entry.rs +++ b/examples/function_todomvc/src/components/entry.rs @@ -2,7 +2,7 @@ use crate::hooks::use_bool_toggle::use_bool_toggle; use crate::state::Entry as Item; use web_sys::{HtmlInputElement, MouseEvent}; use yew::events::{Event, FocusEvent, KeyboardEvent}; -use yew::{function_component, html, Callback, Classes, Properties, TargetCast}; +use yew::prelude::*; #[derive(PartialEq, Properties, Clone)] pub struct EntryProps { diff --git a/examples/function_todomvc/src/components/filter.rs b/examples/function_todomvc/src/components/filter.rs index f295aed06de..c540d14ef58 100644 --- a/examples/function_todomvc/src/components/filter.rs +++ b/examples/function_todomvc/src/components/filter.rs @@ -1,5 +1,5 @@ use crate::state::Filter as FilterEnum; -use yew::{function_component, html, Callback, Properties}; +use yew::prelude::*; #[derive(PartialEq, Properties)] pub struct FilterProps { diff --git a/examples/function_todomvc/src/components/header_input.rs b/examples/function_todomvc/src/components/header_input.rs index d01061b6be2..2cecf3b94ba 100644 --- a/examples/function_todomvc/src/components/header_input.rs +++ b/examples/function_todomvc/src/components/header_input.rs @@ -1,6 +1,6 @@ use web_sys::HtmlInputElement; use yew::events::KeyboardEvent; -use yew::{function_component, html, Callback, Properties, TargetCast}; +use yew::prelude::*; #[derive(PartialEq, Properties, Clone)] pub struct HeaderInputProps { diff --git a/examples/function_todomvc/src/components/info_footer.rs b/examples/function_todomvc/src/components/info_footer.rs index 9c70c6a0074..6b5ac009066 100644 --- a/examples/function_todomvc/src/components/info_footer.rs +++ b/examples/function_todomvc/src/components/info_footer.rs @@ -1,4 +1,4 @@ -use yew::{function_component, html}; +use yew::prelude::*; #[function_component(InfoFooter)] pub fn info_footer() -> Html { diff --git a/examples/function_todomvc/src/main.rs b/examples/function_todomvc/src/main.rs index 644b138ca9d..434f35caea7 100644 --- a/examples/function_todomvc/src/main.rs +++ b/examples/function_todomvc/src/main.rs @@ -1,7 +1,7 @@ use gloo::storage::{LocalStorage, Storage}; use state::{Action, Filter, State}; use strum::IntoEnumIterator; -use yew::{classes, function_component, html, use_effect_with_deps, use_reducer, Callback}; +use yew::prelude::*; mod components; mod hooks; diff --git a/examples/suspense/Cargo.toml b/examples/suspense/Cargo.toml new file mode 100644 index 00000000000..cf1ca96c164 --- /dev/null +++ b/examples/suspense/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "suspense" +version = "0.1.0" +edition = "2018" +license = "MIT OR Apache-2.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +yew = { path = "../../packages/yew" } +gloo-timers = { version = "0.2.2", features = ["futures"] } +wasm-bindgen-futures = "0.4" +wasm-bindgen = "0.2" + +[dependencies.web-sys] +version = "0.3" +features = [ + "HtmlTextAreaElement", +] diff --git a/examples/suspense/README.md b/examples/suspense/README.md new file mode 100644 index 00000000000..137b848a254 --- /dev/null +++ b/examples/suspense/README.md @@ -0,0 +1,10 @@ +# Suspense Example + +[![Demo](https://img.shields.io/website?label=demo&url=https%3A%2F%2Fexamples.yew.rs%2Fsuspense)](https://examples.yew.rs/suspense) + +This is an example that demonstrates `` support. + +## Concepts + +This example shows that how `` works in Yew and how you can +create hooks that utilises suspense. diff --git a/examples/suspense/index.html b/examples/suspense/index.html new file mode 100644 index 00000000000..ec8ff43c363 --- /dev/null +++ b/examples/suspense/index.html @@ -0,0 +1,11 @@ + + + + + Yew Suspense Demo + + + + + + diff --git a/examples/suspense/index.scss b/examples/suspense/index.scss new file mode 100644 index 00000000000..5e3a5385e6a --- /dev/null +++ b/examples/suspense/index.scss @@ -0,0 +1,71 @@ +html, body { + font-family: sans-serif; + + margin: 0; + padding: 0; + + background-color: rgb(237, 244, 255); +} + +.layout { + height: 100vh; + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.content { + height: 600px; + width: 600px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + border-radius: 4px; + box-shadow: 0 0 5px 0 black; + + background: white; +} + +.content-area { + width: 350px; + height: 500px; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +textarea { + width: 300px; + height: 300px; + font-size: 15px; +} + +.action-area { + padding-top: 40px; +} + +button { + color: white; + height: 50px; + width: 300px; + font-size: 20px; + background-color: rgb(88, 164, 255); + border-radius: 5px; + border: none; +} + +.hint { + padding-top: 20px; + + font-size: 12px; + + text-align: center; + + color: rgb(100, 100, 100); +} diff --git a/examples/suspense/src/main.rs b/examples/suspense/src/main.rs new file mode 100644 index 00000000000..31f0a3d3591 --- /dev/null +++ b/examples/suspense/src/main.rs @@ -0,0 +1,60 @@ +use web_sys::HtmlTextAreaElement; +use yew::prelude::*; + +mod use_sleep; + +use use_sleep::use_sleep; + +#[function_component(PleaseWait)] +fn please_wait() -> Html { + html! {
{"Please wait 5 Seconds..."}
} +} + +#[function_component(AppContent)] +fn app_content() -> HtmlResult { + let resleep = use_sleep()?; + + let value = use_state(|| "I am writing a long story...".to_string()); + + let on_text_input = { + let value = value.clone(); + + Callback::from(move |e: InputEvent| { + let input: HtmlTextAreaElement = e.target_unchecked_into(); + + value.set(input.value()); + }) + }; + + let on_take_a_break = Callback::from(move |_| (resleep.clone())()); + + Ok(html! { +
+ +
+ +
{"You can take a break at anytime"}
{"and your work will be preserved."}
+
+
+ }) +} + +#[function_component(App)] +fn app() -> Html { + let fallback = html! {}; + + html! { +
+
+

{"Yew Suspense Demo"}

+ + + +
+
+ } +} + +fn main() { + yew::start_app::(); +} diff --git a/examples/suspense/src/use_sleep.rs b/examples/suspense/src/use_sleep.rs new file mode 100644 index 00000000000..e8e8ff648ce --- /dev/null +++ b/examples/suspense/src/use_sleep.rs @@ -0,0 +1,39 @@ +use std::rc::Rc; +use std::time::Duration; + +use gloo_timers::future::sleep; +use yew::prelude::*; +use yew::suspense::{Suspension, SuspensionResult}; + +#[derive(PartialEq)] +pub struct SleepState { + s: Suspension, +} + +impl SleepState { + fn new() -> Self { + let s = Suspension::from_future(async { + sleep(Duration::from_secs(5)).await; + }); + + Self { s } + } +} + +impl Reducible for SleepState { + type Action = (); + + fn reduce(self: Rc, _action: Self::Action) -> Rc { + Self::new().into() + } +} + +pub fn use_sleep() -> SuspensionResult> { + let sleep_state = use_reducer(SleepState::new); + + if sleep_state.s.resumed() { + Ok(Rc::new(move || sleep_state.dispatch(()))) + } else { + Err(sleep_state.s.clone()) + } +} diff --git a/packages/yew-macro/src/function_component.rs b/packages/yew-macro/src/function_component.rs index f05ed2b2428..e7786a103aa 100644 --- a/packages/yew-macro/src/function_component.rs +++ b/packages/yew-macro/src/function_component.rs @@ -1,11 +1,11 @@ -use proc_macro2::TokenStream; -use quote::{format_ident, quote, quote_spanned, ToTokens}; +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote, ToTokens}; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::token::Comma; +use syn::token::{Comma, Fn}; use syn::{Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, ReturnType, Type, Visibility}; +#[derive(Clone)] pub struct FunctionComponent { block: Box, props_type: Box, @@ -15,127 +15,132 @@ pub struct FunctionComponent { attrs: Vec, name: Ident, return_type: Box, + fn_token: Fn, } impl Parse for FunctionComponent { fn parse(input: ParseStream) -> syn::Result { let parsed: Item = input.parse()?; - match parsed { - Item::Fn(func) => { - let ItemFn { - attrs, - vis, - sig, - block, - } = func; - - if sig.generics.lifetimes().next().is_some() { - return Err(syn::Error::new_spanned( - sig.generics, - "function components can't have generic lifetime parameters", - )); - } + let func = match parsed { + Item::Fn(m) => m, - if sig.asyncness.is_some() { - return Err(syn::Error::new_spanned( - sig.asyncness, - "function components can't be async", - )); - } + item => { + return Err(syn::Error::new_spanned( + item, + "`function_component` attribute can only be applied to functions", + )) + } + }; - if sig.constness.is_some() { - return Err(syn::Error::new_spanned( - sig.constness, - "const functions can't be function components", - )); - } + let ItemFn { + attrs, + vis, + sig, + block, + } = func; - if sig.abi.is_some() { - return Err(syn::Error::new_spanned( - sig.abi, - "extern functions can't be function components", - )); - } + if sig.generics.lifetimes().next().is_some() { + return Err(syn::Error::new_spanned( + sig.generics, + "function components can't have generic lifetime parameters", + )); + } - let return_type = match sig.output { - ReturnType::Default => { + if sig.asyncness.is_some() { + return Err(syn::Error::new_spanned( + sig.asyncness, + "function components can't be async", + )); + } + + if sig.constness.is_some() { + return Err(syn::Error::new_spanned( + sig.constness, + "const functions can't be function components", + )); + } + + if sig.abi.is_some() { + return Err(syn::Error::new_spanned( + sig.abi, + "extern functions can't be function components", + )); + } + + let return_type = match sig.output { + ReturnType::Default => { + return Err(syn::Error::new_spanned( + sig, + "function components must return `yew::Html` or `yew::HtmlResult`", + )) + } + ReturnType::Type(_, ty) => ty, + }; + + let mut inputs = sig.inputs.into_iter(); + let arg = inputs + .next() + .unwrap_or_else(|| syn::parse_quote! { _: &() }); + + let ty = match &arg { + FnArg::Typed(arg) => match &*arg.ty { + Type::Reference(ty) => { + if ty.lifetime.is_some() { return Err(syn::Error::new_spanned( - sig, - "function components must return `yew::Html`", - )) + &ty.lifetime, + "reference must not have a lifetime", + )); } - ReturnType::Type(_, ty) => ty, - }; - - let mut inputs = sig.inputs.into_iter(); - let arg: FnArg = inputs - .next() - .unwrap_or_else(|| syn::parse_quote! { _: &() }); - - let ty = match &arg { - FnArg::Typed(arg) => match &*arg.ty { - Type::Reference(ty) => { - if ty.lifetime.is_some() { - return Err(syn::Error::new_spanned( - &ty.lifetime, - "reference must not have a lifetime", - )); - } - - if ty.mutability.is_some() { - return Err(syn::Error::new_spanned( - &ty.mutability, - "reference must not be mutable", - )); - } - - ty.elem.clone() - } - ty => { - let msg = format!( - "expected a reference to a `Properties` type (try: `&{}`)", - ty.to_token_stream() - ); - return Err(syn::Error::new_spanned(ty, msg)); - } - }, - - FnArg::Receiver(_) => { + + if ty.mutability.is_some() { return Err(syn::Error::new_spanned( - arg, - "function components can't accept a receiver", + &ty.mutability, + "reference must not be mutable", )); } - }; - - // Checking after param parsing may make it a little inefficient - // but that's a requirement for better error messages in case of receivers - // `>0` because first one is already consumed. - if inputs.len() > 0 { - let params: TokenStream = inputs.map(|it| it.to_token_stream()).collect(); - return Err(syn::Error::new_spanned( - params, - "function components can accept at most one parameter for the props", - )); + + ty.elem.clone() + } + ty => { + let msg = format!( + "expected a reference to a `Properties` type (try: `&{}`)", + ty.to_token_stream() + ); + return Err(syn::Error::new_spanned(ty, msg)); } + }, - Ok(Self { - props_type: ty, - block, + FnArg::Receiver(_) => { + return Err(syn::Error::new_spanned( arg, - generics: sig.generics, - vis, - attrs, - name: sig.ident, - return_type, - }) + "function components can't accept a receiver", + )); } - item => Err(syn::Error::new_spanned( - item, - "`function_component` attribute can only be applied to functions", - )), + }; + + // Checking after param parsing may make it a little inefficient + // but that's a requirement for better error messages in case of receivers + // `>0` because first one is already consumed. + if inputs.len() > 0 { + let params: TokenStream = inputs.map(|it| it.to_token_stream()).collect(); + return Err(syn::Error::new_spanned( + params, + "function components can accept at most one parameter for the props", + )); } + + Ok(Self { + props_type: ty, + block, + arg, + generics: sig.generics, + vis, + attrs, + name: sig.ident, + return_type, + fn_token: sig.fn_token, + }) } } @@ -159,63 +164,104 @@ impl Parse for FunctionComponentName { } } +fn print_fn(func_comp: FunctionComponent, use_fn_name: bool) -> TokenStream { + let FunctionComponent { + fn_token, + name, + attrs, + block, + return_type, + generics, + arg, + .. + } = func_comp; + + let (_impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let name = if use_fn_name { + name + } else { + Ident::new("inner", Span::mixed_site()) + }; + + quote! { + #(#attrs)* + #fn_token #name #ty_generics (#arg) -> #return_type + #where_clause + { + #block + } + } +} + pub fn function_component_impl( name: FunctionComponentName, component: FunctionComponent, ) -> syn::Result { let FunctionComponentName { component_name } = name; + let has_separate_name = component_name.is_some(); + + let func = print_fn(component.clone(), has_separate_name); + let FunctionComponent { - block, props_type, - arg, generics, vis, - attrs, name: function_name, - return_type, + .. } = component; let component_name = component_name.unwrap_or_else(|| function_name.clone()); - let function_name = format_ident!( + let provider_name = format_ident!( "{}FunctionProvider", - function_name, - span = function_name.span() + component_name, + span = Span::mixed_site() ); let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - if function_name == component_name { + if has_separate_name && function_name == component_name { return Err(syn::Error::new_spanned( component_name, "the component must not have the same name as the function", )); } - let ret_type = quote_spanned!(return_type.span()=> ::yew::html::Html); - let phantom_generics = generics .type_params() .map(|ty_param| ty_param.ident.clone()) // create a new Punctuated sequence without any type bounds .collect::>(); + let provider_props = Ident::new("props", Span::mixed_site()); + + let fn_generics = ty_generics.as_turbofish(); + + let fn_name = if has_separate_name { + function_name + } else { + Ident::new("inner", Span::mixed_site()) + }; + let quoted = quote! { #[doc(hidden)] #[allow(non_camel_case_types)] #[allow(unused_parens)] - #vis struct #function_name #generics { + #vis struct #provider_name #ty_generics { _marker: ::std::marker::PhantomData<(#phantom_generics)>, } - impl #impl_generics ::yew::functional::FunctionProvider for #function_name #ty_generics #where_clause { + #[automatically_derived] + impl #impl_generics ::yew::functional::FunctionProvider for #provider_name #ty_generics #where_clause { type TProps = #props_type; - fn run(#arg) -> #ret_type { - #block + fn run(#provider_props: &Self::TProps) -> ::yew::html::HtmlResult { + #func + + ::yew::html::IntoHtmlResult::into_html_result(#fn_name #fn_generics (#provider_props)) } } - #(#attrs)* #[allow(type_alias_bounds)] - #vis type #component_name #generics = ::yew::functional::FunctionComponent<#function_name #ty_generics>; + #vis type #component_name #generics = ::yew::functional::FunctionComponent<#provider_name #ty_generics>; }; Ok(quoted) diff --git a/packages/yew-macro/src/html_tree/html_component.rs b/packages/yew-macro/src/html_tree/html_component.rs index 1714d542fb1..53a97236fdf 100644 --- a/packages/yew-macro/src/html_tree/html_component.rs +++ b/packages/yew-macro/src/html_tree/html_component.rs @@ -92,7 +92,7 @@ impl ToTokens for HtmlComponent { children, } = self; - let props_ty = quote_spanned!(ty.span()=> <#ty as ::yew::html::Component>::Properties); + let props_ty = quote_spanned!(ty.span()=> <#ty as ::yew::html::BaseComponent>::Properties); let children_renderer = if children.is_empty() { None } else { diff --git a/packages/yew-macro/tests/function_component_attr/bad-name-fail.stderr b/packages/yew-macro/tests/function_component_attr/bad-name-fail.stderr index 6c97843a4f2..1a6c4c90058 100644 --- a/packages/yew-macro/tests/function_component_attr/bad-name-fail.stderr +++ b/packages/yew-macro/tests/function_component_attr/bad-name-fail.stderr @@ -16,10 +16,8 @@ error: expected identifier 26 | #[function_component(124)] | ^^^ -warning: type `component` should have an upper camel case name +error: the component must not have the same name as the function --> tests/function_component_attr/bad-name-fail.rs:35:22 | 35 | #[function_component(component)] - | ^^^^^^^^^ help: convert the identifier to upper camel case (notice the capitalization): `Component` - | - = note: `#[warn(non_camel_case_types)]` on by default + | ^^^^^^^^^ diff --git a/packages/yew-macro/tests/function_component_attr/bad-return-type-fail.stderr b/packages/yew-macro/tests/function_component_attr/bad-return-type-fail.stderr index 0a5d227dd08..14bca2acab2 100644 --- a/packages/yew-macro/tests/function_component_attr/bad-return-type-fail.stderr +++ b/packages/yew-macro/tests/function_component_attr/bad-return-type-fail.stderr @@ -1,13 +1,14 @@ -error: function components must return `yew::Html` - --> $DIR/bad-return-type-fail.rs:9:1 +error: function components must return `yew::Html` or `yew::HtmlResult` + --> tests/function_component_attr/bad-return-type-fail.rs:9:1 | 9 | fn comp_1(_props: &Props) {} | ^^^^^^^^^^^^^^^^^^^^^^^^^ -error[E0308]: mismatched types - --> $DIR/bad-return-type-fail.rs:13:5 +error[E0277]: the trait bound `u32: IntoHtmlResult` is not satisfied + --> tests/function_component_attr/bad-return-type-fail.rs:11:1 | -12 | fn comp(_props: &Props) -> u32 { - | --- expected `VNode` because of return type -13 | 1 - | ^ expected enum `VNode`, found integer +11 | #[function_component(Comp)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IntoHtmlResult` is not implemented for `u32` + | + = note: required by `into_html_result` + = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/packages/yew-macro/tests/function_component_attr/generic-pass.rs b/packages/yew-macro/tests/function_component_attr/generic-pass.rs index 29df3007d46..3672a4475cb 100644 --- a/packages/yew-macro/tests/function_component_attr/generic-pass.rs +++ b/packages/yew-macro/tests/function_component_attr/generic-pass.rs @@ -58,20 +58,21 @@ fn comp1(_props: &()) -> ::yew::Html { } } -#[::yew::function_component(ConstGenerics)] -fn const_generics() -> ::yew::Html { - ::yew::html! { -
- { N } -
- } -} +// no longer possible? +// #[::yew::function_component(ConstGenerics)] +// fn const_generics() -> ::yew::Html { +// ::yew::html! { +//
+// { N } +//
+// } +// } fn compile_pass() { ::yew::html! { a=10 /> }; ::yew::html! { /> }; - ::yew::html! { /> }; + // ::yew::html! { /> }; } fn main() {} diff --git a/packages/yew-macro/tests/function_component_attr/generic-props-fail.stderr b/packages/yew-macro/tests/function_component_attr/generic-props-fail.stderr index 5d0a9b68e0d..9c058ec41ec 100644 --- a/packages/yew-macro/tests/function_component_attr/generic-props-fail.stderr +++ b/packages/yew-macro/tests/function_component_attr/generic-props-fail.stderr @@ -16,27 +16,41 @@ error[E0599]: no method named `build` found for struct `PropsBuilder /> }; | ^^^^ method not found in `PropsBuilder` -error[E0277]: the trait bound `MissingTypeBounds: yew::Properties` is not satisfied +error[E0277]: the trait bound `FunctionComponent>: BaseComponent` is not satisfied --> tests/function_component_attr/generic-props-fail.rs:27:14 | 27 | html! { /> }; - | ^^^^ the trait `yew::Properties` is not implemented for `MissingTypeBounds` + | ^^^^ the trait `BaseComponent` is not implemented for `FunctionComponent>` | - = note: required because of the requirements on the impl of `FunctionProvider` for `compFunctionProvider` + = help: the following implementations were found: + as BaseComponent> -error[E0599]: the function or associated item `new` exists for struct `VChild>>`, but its trait bounds were not satisfied +error[E0599]: the function or associated item `new` exists for struct `VChild>>`, but its trait bounds were not satisfied --> tests/function_component_attr/generic-props-fail.rs:27:14 | 27 | html! { /> }; - | ^^^^ function or associated item cannot be called on `VChild>>` due to unsatisfied trait bounds + | ^^^^ function or associated item cannot be called on `VChild>>` due to unsatisfied trait bounds | ::: $WORKSPACE/packages/yew/src/functional/mod.rs | | pub struct FunctionComponent { - | ----------------------------------------------------------- doesn't satisfy `_: yew::Component` + | ----------------------------------------------------------- doesn't satisfy `_: BaseComponent` | = note: the following trait bounds were not satisfied: - `FunctionComponent>: yew::Component` + `FunctionComponent>: BaseComponent` + +error[E0277]: the trait bound `MissingTypeBounds: yew::Properties` is not satisfied + --> tests/function_component_attr/generic-props-fail.rs:27:14 + | +27 | html! { /> }; + | ^^^^ the trait `yew::Properties` is not implemented for `MissingTypeBounds` + | + ::: $WORKSPACE/packages/yew/src/functional/mod.rs + | + | pub struct FunctionComponent { + | ---------------- required by this bound in `FunctionComponent` + | + = note: required because of the requirements on the impl of `FunctionProvider` for `CompFunctionProvider` error[E0107]: missing generics for type alias `Comp` --> tests/function_component_attr/generic-props-fail.rs:30:14 diff --git a/packages/yew-macro/tests/html_macro/component-unimplemented-fail.stderr b/packages/yew-macro/tests/html_macro/component-unimplemented-fail.stderr index 055b47b4db9..4b062ceeda5 100644 --- a/packages/yew-macro/tests/html_macro/component-unimplemented-fail.stderr +++ b/packages/yew-macro/tests/html_macro/component-unimplemented-fail.stderr @@ -3,15 +3,17 @@ error[E0277]: the trait bound `Unimplemented: yew::Component` is not satisfied | 6 | html! { }; | ^^^^^^^^^^^^^ the trait `yew::Component` is not implemented for `Unimplemented` + | + = note: required because of the requirements on the impl of `BaseComponent` for `Unimplemented` error[E0599]: the function or associated item `new` exists for struct `VChild`, but its trait bounds were not satisfied --> tests/html_macro/component-unimplemented-fail.rs:6:14 | 3 | struct Unimplemented; - | --------------------- doesn't satisfy `Unimplemented: yew::Component` + | --------------------- doesn't satisfy `Unimplemented: BaseComponent` ... 6 | html! { }; | ^^^^^^^^^^^^^ function or associated item cannot be called on `VChild` due to unsatisfied trait bounds | = note: the following trait bounds were not satisfied: - `Unimplemented: yew::Component` + `Unimplemented: BaseComponent` diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 77d23b65ee9..9d30ad38267 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -25,6 +25,7 @@ slab = "0.4" wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" yew-macro = { version = "^0.19.0", path = "../yew-macro" } +thiserror = "1.0" scoped-tls-hkt = "0.1" @@ -62,6 +63,7 @@ features = [ [dev-dependencies] easybench-wasm = "0.2" wasm-bindgen-test = "0.3" +gloo = { version = "0.4", features = ["futures"] } [features] doc_test = [] diff --git a/packages/yew/src/app_handle.rs b/packages/yew/src/app_handle.rs index 8ac8cfcc8f8..04e3b79afd7 100644 --- a/packages/yew/src/app_handle.rs +++ b/packages/yew/src/app_handle.rs @@ -3,21 +3,21 @@ use std::ops::Deref; -use crate::html::{Component, NodeRef, Scope, Scoped}; +use crate::html::{BaseComponent, NodeRef, Scope, Scoped}; use gloo_utils::document; use std::rc::Rc; use web_sys::Element; /// An instance of an application. #[derive(Debug)] -pub struct AppHandle { +pub struct AppHandle { /// `Scope` holder pub(crate) scope: Scope, } impl AppHandle where - COMP: Component, + COMP: BaseComponent, { /// The main entry point of a Yew program which also allows passing properties. It works /// similarly to the `program` function in Elm. You should provide an initial model, `update` @@ -56,7 +56,7 @@ where impl Deref for AppHandle where - COMP: Component, + COMP: BaseComponent, { type Target = Scope; diff --git a/packages/yew/src/functional/hooks/use_ref.rs b/packages/yew/src/functional/hooks/use_ref.rs index cf633d26045..d334a4bbee2 100644 --- a/packages/yew/src/functional/hooks/use_ref.rs +++ b/packages/yew/src/functional/hooks/use_ref.rs @@ -77,7 +77,8 @@ pub fn use_ref(initial_value: impl FnOnce() -> T) -> Rc { /// ```rust /// # use wasm_bindgen::{prelude::Closure, JsCast}; /// # use yew::{ -/// # function_component, html, use_effect_with_deps, use_node_ref +/// # function_component, html, use_effect_with_deps, use_node_ref, +/// # Html, /// # }; /// # use web_sys::{Event, HtmlElement}; /// diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index aa3df703725..cd0d449ccd1 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -1,6 +1,6 @@ //! Function components are a simplified version of normal components. //! They consist of a single function annotated with the attribute `#[function_component(_)]` -//! that receives props and determines what should be rendered by returning [`Html`]. +//! that receives props and determines what should be rendered by returning [`Html`](crate::Html). //! //! ```rust //! # use yew::prelude::*; @@ -13,8 +13,8 @@ //! //! More details about function components and Hooks can be found on [Yew Docs](https://yew.rs/docs/next/concepts/function-components/introduction) -use crate::html::AnyScope; -use crate::{Component, Html, Properties}; +use crate::html::{AnyScope, BaseComponent, HtmlResult}; +use crate::Properties; use scoped_tls_hkt::scoped_thread_local; use std::cell::RefCell; use std::fmt; @@ -70,10 +70,10 @@ pub trait FunctionProvider { /// Properties for the Function Component. type TProps: Properties + PartialEq; - /// Render the component. This function returns the [`Html`] to be rendered for the component. + /// Render the component. This function returns the [`Html`](crate::Html) to be rendered for the component. /// - /// Equivalent of [`Component::view`]. - fn run(props: &Self::TProps) -> Html; + /// Equivalent of [`Component::view`](crate::html::Component::view). + fn run(props: &Self::TProps) -> HtmlResult; } /// Wrapper that allows a struct implementing [`FunctionProvider`] to be consumed as a component. @@ -100,7 +100,7 @@ where } } -impl Component for FunctionComponent +impl BaseComponent for FunctionComponent where T: FunctionProvider, { @@ -137,7 +137,11 @@ where msg() } - fn view(&self, ctx: &Context) -> Html { + fn changed(&mut self, _ctx: &Context) -> bool { + true + } + + fn view(&self, ctx: &Context) -> HtmlResult { self.with_hook_state(|| T::run(&*ctx.props())) } @@ -192,6 +196,7 @@ pub struct HookUpdater { hook: Rc>, process_message: ProcessMessage, } + impl HookUpdater { /// Callback which runs the hook. pub fn callback(&self, cb: F) diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 49931d551c2..e1b7a5e4108 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -1,13 +1,16 @@ //! Component lifecycle module -use super::{Component, Scope}; +use super::{AnyScope, BaseComponent, Scope}; +use crate::html::RenderError; use crate::scheduler::{self, Runnable, Shared}; +use crate::suspense::{Suspense, Suspension}; use crate::virtual_dom::{VDiff, VNode}; +use crate::Callback; use crate::{Context, NodeRef}; use std::rc::Rc; use web_sys::Element; -pub(crate) struct ComponentState { +pub(crate) struct ComponentState { pub(crate) component: Box, pub(crate) root_node: VNode, @@ -17,12 +20,14 @@ pub(crate) struct ComponentState { node_ref: NodeRef, has_rendered: bool, + suspension: Option, + // Used for debug logging #[cfg(debug_assertions)] pub(crate) vcomp_id: u64, } -impl ComponentState { +impl ComponentState { pub(crate) fn new( parent: Element, next_sibling: NodeRef, @@ -47,6 +52,7 @@ impl ComponentState { parent, next_sibling, node_ref, + suspension: None, has_rendered: false, #[cfg(debug_assertions)] @@ -55,7 +61,7 @@ impl ComponentState { } } -pub(crate) struct CreateRunner { +pub(crate) struct CreateRunner { pub(crate) parent: Element, pub(crate) next_sibling: NodeRef, pub(crate) placeholder: VNode, @@ -64,7 +70,7 @@ pub(crate) struct CreateRunner { pub(crate) scope: Scope, } -impl Runnable for CreateRunner { +impl Runnable for CreateRunner { fn run(self: Box) { let mut current_state = self.scope.state.borrow_mut(); if current_state.is_none() { @@ -83,21 +89,23 @@ impl Runnable for CreateRunner { } } -pub(crate) enum UpdateEvent { +pub(crate) enum UpdateEvent { /// Wraps messages for a component. Message(COMP::Message), /// Wraps batch of messages for a component. MessageBatch(Vec), /// Wraps properties, node ref, and next sibling for a component. Properties(Rc, NodeRef, NodeRef), + /// Shift Scope. + Shift(Element, NodeRef), } -pub(crate) struct UpdateRunner { +pub(crate) struct UpdateRunner { pub(crate) state: Shared>>, pub(crate) event: UpdateEvent, } -impl Runnable for UpdateRunner { +impl Runnable for UpdateRunner { fn run(self: Box) { if let Some(mut state) = self.state.borrow_mut().as_mut() { let schedule_render = match self.event { @@ -120,6 +128,16 @@ impl Runnable for UpdateRunner { false } } + UpdateEvent::Shift(parent, next_sibling) => { + state + .root_node + .shift(&state.parent, &parent, next_sibling.clone()); + + state.parent = parent; + state.next_sibling = next_sibling; + + false + } }; #[cfg(debug_assertions)] @@ -144,11 +162,11 @@ impl Runnable for UpdateRunner { } } -pub(crate) struct DestroyRunner { +pub(crate) struct DestroyRunner { pub(crate) state: Shared>>, } -impl Runnable for DestroyRunner { +impl Runnable for DestroyRunner { fn run(self: Box) { if let Some(mut state) = self.state.borrow_mut().take() { #[cfg(debug_assertions)] @@ -161,33 +179,99 @@ impl Runnable for DestroyRunner { } } -pub(crate) struct RenderRunner { +pub(crate) struct RenderRunner { pub(crate) state: Shared>>, } -impl Runnable for RenderRunner { +impl Runnable for RenderRunner { fn run(self: Box) { if let Some(state) = self.state.borrow_mut().as_mut() { #[cfg(debug_assertions)] crate::virtual_dom::vcomp::log_event(state.vcomp_id, "render"); - let mut new_root = state.component.view(&state.context); - std::mem::swap(&mut new_root, &mut state.root_node); - let ancestor = Some(new_root); - let new_root = &mut state.root_node; - let scope = state.context.scope.clone().into(); - let next_sibling = state.next_sibling.clone(); - let node = new_root.apply(&scope, &state.parent, next_sibling, ancestor); - state.node_ref.link(node); + match state.component.view(&state.context) { + Ok(m) => { + // Currently not suspended, we remove any previous suspension and update + // normally. + let mut root = m; + std::mem::swap(&mut root, &mut state.root_node); + + if let Some(ref m) = state.suspension { + let comp_scope = AnyScope::from(state.context.scope.clone()); + + let suspense_scope = comp_scope.find_parent_scope::().unwrap(); + let suspense = suspense_scope.get_component().unwrap(); + + suspense.resume(m.clone()); + } + + let ancestor = Some(root); + let new_root = &mut state.root_node; + let scope = state.context.scope.clone().into(); + let next_sibling = state.next_sibling.clone(); + + let node = new_root.apply(&scope, &state.parent, next_sibling, ancestor); + state.node_ref.link(node); + } + + Err(RenderError::Suspended(m)) => { + // Currently suspended, we re-use previous root node and send + // suspension to parent element. + let shared_state = self.state.clone(); + + if m.resumed() { + // schedule a render immediately if suspension is resumed. + + scheduler::push_component_render( + shared_state.as_ptr() as usize, + RenderRunner { + state: shared_state.clone(), + }, + RenderedRunner { + state: shared_state, + }, + ); + } else { + // We schedule a render after current suspension is resumed. + + let comp_scope = AnyScope::from(state.context.scope.clone()); + + let suspense_scope = comp_scope.find_parent_scope::().unwrap(); + let suspense = suspense_scope.get_component().unwrap(); + + m.listen(Callback::from(move |_| { + scheduler::push_component_render( + shared_state.as_ptr() as usize, + RenderRunner { + state: shared_state.clone(), + }, + RenderedRunner { + state: shared_state.clone(), + }, + ); + })); + + if let Some(ref last_m) = state.suspension { + if &m != last_m { + // We remove previous suspension from the suspense. + suspense.resume(last_m.clone()); + } + } + state.suspension = Some(m.clone()); + + suspense.suspend(m); + } + } + }; } } } -pub(crate) struct RenderedRunner { +pub(crate) struct RenderedRunner { pub(crate) state: Shared>>, } -impl Runnable for RenderedRunner { +impl Runnable for RenderedRunner { fn run(self: Box) { if let Some(state) = self.state.borrow_mut().as_mut() { #[cfg(debug_assertions)] diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs index 59c7d8bcd02..5ddc34c399b 100644 --- a/packages/yew/src/html/component/mod.rs +++ b/packages/yew/src/html/component/mod.rs @@ -5,7 +5,7 @@ mod lifecycle; mod properties; mod scope; -use super::Html; +use super::{Html, HtmlResult, IntoHtmlResult}; pub use children::*; pub use properties::*; pub(crate) use scope::Scoped; @@ -15,12 +15,12 @@ use std::rc::Rc; /// The [`Component`]'s context. This contains component's [`Scope`] and and props and /// is passed to every lifecycle method. #[derive(Debug)] -pub struct Context { +pub struct Context { pub(crate) scope: Scope, pub(crate) props: Rc, } -impl Context { +impl Context { /// The component scope #[inline] pub fn link(&self) -> &Scope { @@ -34,6 +34,38 @@ impl Context { } } +/// The common base of both function components and struct components. +/// +/// If you are taken here by doc links, you might be looking for [`Component`] or +/// [`#[function_component]`](crate::functional::function_component). +/// +/// We provide a blanket implementation of this trait for every member that implements [`Component`]. +pub trait BaseComponent: Sized + 'static { + /// The Component's Message. + type Message: 'static; + + /// The Component's properties. + type Properties: Properties; + + /// Creates a component. + fn create(ctx: &Context) -> Self; + + /// Updates component's internal state. + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool; + + /// React to changes of component properties. + fn changed(&mut self, ctx: &Context) -> bool; + + /// Returns a component layout to be rendered. + fn view(&self, ctx: &Context) -> HtmlResult; + + /// Notified after a layout is rendered. + fn rendered(&mut self, ctx: &Context, first_render: bool); + + /// Notified before a component is destroyed. + fn destroy(&mut self, ctx: &Context); +} + /// Components are the basic building blocks of the UI in a Yew app. Each Component /// chooses how to display itself using received props and self-managed state. /// Components can be dynamic and interactive by declaring messages that are @@ -95,3 +127,36 @@ pub trait Component: Sized + 'static { #[allow(unused_variables)] fn destroy(&mut self, ctx: &Context) {} } + +impl BaseComponent for T +where + T: Sized + Component + 'static, +{ + type Message = ::Message; + + type Properties = ::Properties; + + fn create(ctx: &Context) -> Self { + Component::create(ctx) + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + Component::update(self, ctx, msg) + } + + fn changed(&mut self, ctx: &Context) -> bool { + Component::changed(self, ctx) + } + + fn view(&self, ctx: &Context) -> HtmlResult { + Component::view(self, ctx).into_html_result() + } + + fn rendered(&mut self, ctx: &Context, first_render: bool) { + Component::rendered(self, ctx, first_render) + } + + fn destroy(&mut self, ctx: &Context) { + Component::destroy(self, ctx) + } +} diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 484f3918eb9..410f9c34530 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -5,7 +5,7 @@ use super::{ ComponentState, CreateRunner, DestroyRunner, RenderRunner, RenderedRunner, UpdateEvent, UpdateRunner, }, - Component, + BaseComponent, }; use crate::callback::Callback; use crate::context::{ContextHandle, ContextProvider}; @@ -34,7 +34,7 @@ pub struct AnyScope { pub(crate) vcomp_id: u64, } -impl From> for AnyScope { +impl From> for AnyScope { fn from(scope: Scope) -> Self { AnyScope { type_id: TypeId::of::(), @@ -71,7 +71,7 @@ impl AnyScope { } /// Attempts to downcast into a typed scope - pub fn downcast(self) -> Scope { + pub fn downcast(self) -> Scope { let state = self .state .downcast::>>>() @@ -93,7 +93,7 @@ impl AnyScope { } } - fn find_parent_scope(&self) -> Option> { + pub(crate) fn find_parent_scope(&self) -> Option> { let expected_type_id = TypeId::of::(); iter::successors(Some(self), |scope| scope.get_parent()) .filter(|scope| scope.get_type_id() == &expected_type_id) @@ -119,9 +119,10 @@ pub(crate) trait Scoped { fn to_any(&self) -> AnyScope; fn root_vnode(&self) -> Option>; fn destroy(&mut self); + fn shift_node(&self, parent: Element, next_sibling: NodeRef); } -impl Scoped for Scope { +impl Scoped for Scope { fn to_any(&self) -> AnyScope { self.clone().into() } @@ -145,10 +146,17 @@ impl Scoped for Scope { // Not guaranteed to already have the scheduler started scheduler::start(); } + + fn shift_node(&self, parent: Element, next_sibling: NodeRef) { + scheduler::push_component_update(UpdateRunner { + state: self.state.clone(), + event: UpdateEvent::Shift(parent, next_sibling), + }); + } } /// A context which allows sending messages to a component. -pub struct Scope { +pub struct Scope { parent: Option>, pub(crate) state: Shared>>, @@ -157,13 +165,13 @@ pub struct Scope { pub(crate) vcomp_id: u64, } -impl fmt::Debug for Scope { +impl fmt::Debug for Scope { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("Scope<_>") } } -impl Clone for Scope { +impl Clone for Scope { fn clone(&self) -> Self { Scope { parent: self.parent.clone(), @@ -175,7 +183,7 @@ impl Clone for Scope { } } -impl Scope { +impl Scope { /// Returns the parent scope pub fn get_parent(&self) -> Option<&AnyScope> { self.parent.as_deref() @@ -268,7 +276,7 @@ impl Scope { /// Send a message to the component. /// /// Please be aware that currently this method synchronously - /// schedules a call to the [Component](Component) interface. + /// schedules a call to the [Component](crate::html::Component) interface. pub fn send_message(&self, msg: T) where T: Into, @@ -283,7 +291,7 @@ impl Scope { /// function is called only once if needed. /// /// Please be aware that currently this method synchronously - /// schedules calls to the [Component](Component) interface. + /// schedules calls to the [Component](crate::html::Component) interface. pub fn send_message_batch(&self, messages: Vec) { // There is no reason to schedule empty batches. // This check is especially handy for the batch_callback method. @@ -340,7 +348,6 @@ impl Scope { }; closure.into() } - /// This method creates a [`Callback`] which returns a Future which /// returns a message to be sent back to the component's event /// loop. @@ -409,7 +416,7 @@ impl Scope { /// Defines a message type that can be sent to a component. /// Used for the return value of closure given to [Scope::batch_callback](struct.Scope.html#method.batch_callback). -pub trait SendAsMessage { +pub trait SendAsMessage { /// Sends the message to the given component's scope. /// See [Scope::batch_callback](struct.Scope.html#method.batch_callback). fn send(self, scope: &Scope); @@ -417,7 +424,7 @@ pub trait SendAsMessage { impl SendAsMessage for Option where - COMP: Component, + COMP: BaseComponent, { fn send(self, scope: &Scope) { if let Some(msg) = self { @@ -428,7 +435,7 @@ where impl SendAsMessage for Vec where - COMP: Component, + COMP: BaseComponent, { fn send(self, scope: &Scope) { scope.send_message_batch(self); diff --git a/packages/yew/src/html/conversion.rs b/packages/yew/src/html/conversion.rs index 36b9246d5ee..6ba7766bce8 100644 --- a/packages/yew/src/html/conversion.rs +++ b/packages/yew/src/html/conversion.rs @@ -2,7 +2,7 @@ use super::{Component, NodeRef, Scope}; use crate::virtual_dom::AttrValue; use std::{borrow::Cow, rc::Rc}; -/// Marker trait for types that the [`html!`] macro may clone implicitly. +/// Marker trait for types that the [`html!`](macro@crate::html) macro may clone implicitly. pub trait ImplicitClone: Clone {} // this is only implemented because there's no way to avoid cloning this value diff --git a/packages/yew/src/html/error.rs b/packages/yew/src/html/error.rs new file mode 100644 index 00000000000..f96cac9003d --- /dev/null +++ b/packages/yew/src/html/error.rs @@ -0,0 +1,14 @@ +use thiserror::Error; + +use crate::suspense::Suspension; + +/// Render Error. +#[derive(Error, Debug, Clone, PartialEq)] +pub enum RenderError { + /// Component Rendering Suspended + #[error("component rendering is suspended.")] + Suspended(#[from] Suspension), +} + +/// Render Result. +pub type RenderResult = std::result::Result; diff --git a/packages/yew/src/html/mod.rs b/packages/yew/src/html/mod.rs index 84999370621..ae290b1e67f 100644 --- a/packages/yew/src/html/mod.rs +++ b/packages/yew/src/html/mod.rs @@ -3,13 +3,16 @@ mod classes; mod component; mod conversion; +mod error; mod listener; pub use classes::*; pub use component::*; pub use conversion::*; +pub use error::*; pub use listener::*; +use crate::sealed::Sealed; use crate::virtual_dom::{VNode, VPortal}; use std::cell::RefCell; use std::rc::Rc; @@ -19,6 +22,31 @@ use web_sys::{Element, Node}; /// A type which expected as a result of `view` function implementation. pub type Html = VNode; +/// An enhanced type of `Html` returned in suspendible function components. +pub type HtmlResult = RenderResult; + +impl Sealed for HtmlResult {} +impl Sealed for Html {} + +/// A trait to translate into a [`HtmlResult`]. +pub trait IntoHtmlResult: Sealed { + /// Performs the conversion. + fn into_html_result(self) -> HtmlResult; +} + +impl IntoHtmlResult for HtmlResult { + #[inline(always)] + fn into_html_result(self) -> HtmlResult { + self + } +} +impl IntoHtmlResult for Html { + #[inline(always)] + fn into_html_result(self) -> HtmlResult { + Ok(self) + } +} + /// Wrapped Node reference for later use in Component lifecycle methods. /// /// # Example diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index 51783e9c06b..d988d2eccdf 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -160,7 +160,6 @@ pub use yew_macro::html; /// impl Into for ListItem { /// fn into(self) -> Html { html! { } } /// } -/// /// // You can use `List` with nested `ListItem` components. /// // Using any other kind of element would result in a compile error. /// # fn test() -> Html { @@ -262,6 +261,8 @@ pub mod context; pub mod functional; pub mod html; pub mod scheduler; +mod sealed; +pub mod suspense; #[cfg(test)] pub mod tests; pub mod utils; @@ -283,6 +284,8 @@ pub mod events { pub use crate::app_handle::AppHandle; use web_sys::Element; +use crate::html::BaseComponent; + thread_local! { static PANIC_HOOK_IS_SET: Cell = Cell::new(false); } @@ -305,7 +308,7 @@ fn set_default_panic_hook() { /// If you would like to pass props, use the `start_app_with_props_in_element` method. pub fn start_app_in_element(element: Element) -> AppHandle where - COMP: Component, + COMP: BaseComponent, COMP::Properties: Default, { start_app_with_props_in_element(element, COMP::Properties::default()) @@ -315,7 +318,7 @@ where /// Alias to start_app_in_element(Body) pub fn start_app() -> AppHandle where - COMP: Component, + COMP: BaseComponent, COMP::Properties: Default, { start_app_with_props(COMP::Properties::default()) @@ -328,7 +331,7 @@ where /// CSS classes of the body element. pub fn start_app_as_body() -> AppHandle where - COMP: Component, + COMP: BaseComponent, COMP::Properties: Default, { start_app_with_props_as_body(COMP::Properties::default()) @@ -341,7 +344,7 @@ pub fn start_app_with_props_in_element( props: COMP::Properties, ) -> AppHandle where - COMP: Component, + COMP: BaseComponent, { set_default_panic_hook(); AppHandle::::mount_with_props(element, Rc::new(props)) @@ -351,7 +354,7 @@ where /// This function does the same as `start_app(...)` but allows to start an Yew application with properties. pub fn start_app_with_props(props: COMP::Properties) -> AppHandle where - COMP: Component, + COMP: BaseComponent, { start_app_with_props_in_element( gloo_utils::document() @@ -369,7 +372,7 @@ where /// CSS classes of the body element. pub fn start_app_with_props_as_body(props: COMP::Properties) -> AppHandle where - COMP: Component, + COMP: BaseComponent, { set_default_panic_hook(); AppHandle::::mount_as_body_with_props(Rc::new(props)) @@ -389,10 +392,11 @@ pub mod prelude { pub use crate::context::ContextProvider; pub use crate::events::*; pub use crate::html::{ - create_portal, Children, ChildrenWithProps, Classes, Component, Context, Html, NodeRef, - Properties, + create_portal, BaseComponent, Children, ChildrenWithProps, Classes, Component, Context, + Html, HtmlResult, NodeRef, Properties, }; pub use crate::macros::{classes, html, html_nested}; + pub use crate::suspense::Suspense; pub use crate::functional::*; } diff --git a/packages/yew/src/sealed.rs b/packages/yew/src/sealed.rs new file mode 100644 index 00000000000..8d8bbad8650 --- /dev/null +++ b/packages/yew/src/sealed.rs @@ -0,0 +1,2 @@ +/// Base traits for sealed traits. +pub trait Sealed {} diff --git a/packages/yew/src/suspense/component.rs b/packages/yew/src/suspense/component.rs new file mode 100644 index 00000000000..750f2d9f78b --- /dev/null +++ b/packages/yew/src/suspense/component.rs @@ -0,0 +1,99 @@ +use crate::html::{Children, Component, Context, Html, Properties, Scope}; +use crate::virtual_dom::{Key, VList, VNode, VSuspense}; + +use gloo_utils::document; +use web_sys::Element; + +use super::Suspension; + +#[derive(Properties, PartialEq, Debug, Clone)] +pub struct SuspenseProps { + #[prop_or_default] + pub children: Children, + + #[prop_or_default] + pub fallback: Html, + + #[prop_or_default] + pub key: Option, +} + +#[derive(Debug)] +pub enum SuspenseMsg { + Suspend(Suspension), + Resume(Suspension), +} + +/// Suspend rendering and show a fallback UI until the underlying task completes. +#[derive(Debug)] +pub struct Suspense { + link: Scope, + suspensions: Vec, + detached_parent: Element, +} + +impl Component for Suspense { + type Properties = SuspenseProps; + type Message = SuspenseMsg; + + fn create(ctx: &Context) -> Self { + Self { + link: ctx.link().clone(), + suspensions: Vec::new(), + detached_parent: document().create_element("div").unwrap(), + } + } + + fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { + match msg { + Self::Message::Suspend(m) => { + if m.resumed() { + return false; + } + + m.listen(self.link.callback(Self::Message::Resume)); + + self.suspensions.push(m); + + true + } + Self::Message::Resume(ref m) => { + let suspensions_len = self.suspensions.len(); + self.suspensions.retain(|n| m != n); + + suspensions_len != self.suspensions.len() + } + } + } + + fn view(&self, ctx: &Context) -> Html { + let SuspenseProps { + children, + fallback: fallback_vnode, + key, + } = (*ctx.props()).clone(); + + let children_vnode = + VNode::from(VList::with_children(children.into_iter().collect(), None)); + + let vsuspense = VSuspense::new( + children_vnode, + fallback_vnode, + self.detached_parent.clone(), + !self.suspensions.is_empty(), + key, + ); + + VNode::from(vsuspense) + } +} + +impl Suspense { + pub(crate) fn suspend(&self, s: Suspension) { + self.link.send_message(SuspenseMsg::Suspend(s)); + } + + pub(crate) fn resume(&self, s: Suspension) { + self.link.send_message(SuspenseMsg::Resume(s)); + } +} diff --git a/packages/yew/src/suspense/mod.rs b/packages/yew/src/suspense/mod.rs new file mode 100644 index 00000000000..617c263775f --- /dev/null +++ b/packages/yew/src/suspense/mod.rs @@ -0,0 +1,7 @@ +//! This module provides suspense support. + +mod component; +mod suspension; + +pub use component::Suspense; +pub use suspension::{Suspension, SuspensionHandle, SuspensionResult}; diff --git a/packages/yew/src/suspense/suspension.rs b/packages/yew/src/suspense/suspension.rs new file mode 100644 index 00000000000..9430e8d6dd5 --- /dev/null +++ b/packages/yew/src/suspense/suspension.rs @@ -0,0 +1,140 @@ +use std::cell::RefCell; +use std::future::Future; +use std::pin::Pin; +use std::rc::Rc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::task::{Context, Poll}; + +use thiserror::Error; +use wasm_bindgen_futures::spawn_local; + +use crate::Callback; + +thread_local! { + static SUSPENSION_ID: RefCell = RefCell::default(); +} + +/// A Suspension. +/// +/// This type can be sent back as an `Err(_)` to suspend a component until the underlying task +/// completes. +#[derive(Error, Debug, Clone)] +#[error("suspend component rendering")] +pub struct Suspension { + id: usize, + listeners: Rc>>>, + + resumed: Rc, +} + +impl PartialEq for Suspension { + fn eq(&self, rhs: &Self) -> bool { + self.id == rhs.id + } +} + +impl Suspension { + /// Creates a Suspension. + pub fn new() -> (Self, SuspensionHandle) { + let id = SUSPENSION_ID.with(|m| { + let mut m = m.borrow_mut(); + *m += 1; + + *m + }); + + let self_ = Suspension { + id, + listeners: Rc::default(), + resumed: Rc::default(), + }; + + (self_.clone(), SuspensionHandle { inner: self_ }) + } + + /// Creates a Suspension that resumes when the [`Future`] resolves. + pub fn from_future(f: impl Future + 'static) -> Self { + let (self_, handle) = Self::new(); + + spawn_local(async move { + f.await; + handle.resume(); + }); + + self_ + } + + /// Returns `true` if the current suspension is already resumed. + pub fn resumed(&self) -> bool { + self.resumed.load(Ordering::Relaxed) + } + + /// Listens to a suspension and get notified when it resumes. + pub(crate) fn listen(&self, cb: Callback) { + if self.resumed() { + cb.emit(self.clone()); + return; + } + + let mut listeners = self.listeners.borrow_mut(); + + listeners.push(cb); + } + + fn resume_by_ref(&self) { + // The component can resume rendering by returning a non-suspended result after a state is + // updated, so we always need to check here. + if !self.resumed() { + self.resumed.store(true, Ordering::Relaxed); + let listeners = self.listeners.borrow(); + + for listener in listeners.iter() { + listener.emit(self.clone()); + } + } + } +} + +impl Future for Suspension { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self.resumed() { + return Poll::Ready(()); + } + + let waker = cx.waker().clone(); + self.listen(Callback::from(move |_| { + waker.clone().wake(); + })); + + Poll::Pending + } +} + +/// A Suspension Result. +pub type SuspensionResult = std::result::Result; + +/// A Suspension Handle. +/// +/// This type is used to control the corresponding [`Suspension`]. +/// +/// When the current struct is dropped or `resume` is called, it will resume rendering of current +/// component. +#[derive(Debug, PartialEq)] +pub struct SuspensionHandle { + inner: Suspension, +} + +impl SuspensionHandle { + /// Resumes component rendering. + pub fn resume(self) { + self.inner.resume_by_ref(); + } +} + +impl Drop for SuspensionHandle { + fn drop(&mut self) { + self.inner.resume_by_ref(); + } +} diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs index 296a08aa98f..b09ce562b17 100644 --- a/packages/yew/src/virtual_dom/mod.rs +++ b/packages/yew/src/virtual_dom/mod.rs @@ -13,6 +13,8 @@ pub mod vnode; #[doc(hidden)] pub mod vportal; #[doc(hidden)] +pub mod vsuspense; +#[doc(hidden)] pub mod vtag; #[doc(hidden)] pub mod vtext; @@ -36,6 +38,8 @@ pub use self::vnode::VNode; #[doc(inline)] pub use self::vportal::VPortal; #[doc(inline)] +pub use self::vsuspense::VSuspense; +#[doc(inline)] pub use self::vtag::VTag; #[doc(inline)] pub use self::vtext::VText; @@ -224,7 +228,7 @@ pub enum Attributes { /// Attribute keys. Includes both always set and optional attribute keys. keys: &'static [&'static str], - /// Attribute values. Matches [keys]. Optional attributes are designated by setting [None]. + /// Attribute values. Matches [keys](Attributes::Dynamic::keys). Optional attributes are designated by setting [None]. values: Box<[Option]>, }, @@ -495,6 +499,12 @@ pub(crate) trait VDiff { /// Remove self from parent. fn detach(&mut self, parent: &Element); + /// Move elements from one parent to another parent. + /// This is currently only used by `VSuspense` to preserve component state without detaching + /// (which destroys component state). + /// Prefer `detach` then apply if possible. + fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef); + /// Scoped diff apply to other tree. /// /// Virtual rendering for the node. It uses parent node and existing diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index 687a4e68869..a0055b20697 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -1,7 +1,7 @@ //! This module contains the implementation of a virtual component (`VComp`). use super::{Key, VDiff, VNode}; -use crate::html::{AnyScope, Component, NodeRef, Scope, Scoped}; +use crate::html::{AnyScope, BaseComponent, NodeRef, Scope, Scoped}; use std::any::TypeId; use std::borrow::Borrow; use std::fmt; @@ -73,7 +73,7 @@ impl Clone for VComp { } /// A virtual child component. -pub struct VChild { +pub struct VChild { /// The component properties pub props: Rc, /// Reference to the mounted node @@ -81,7 +81,7 @@ pub struct VChild { key: Option, } -impl Clone for VChild { +impl Clone for VChild { fn clone(&self) -> Self { VChild { props: Rc::clone(&self.props), @@ -91,7 +91,7 @@ impl Clone for VChild { } } -impl PartialEq for VChild +impl PartialEq for VChild where COMP::Properties: PartialEq, { @@ -102,7 +102,7 @@ where impl VChild where - COMP: Component, + COMP: BaseComponent, { /// Creates a child component that can be accessed and modified by its parent. pub fn new(props: COMP::Properties, node_ref: NodeRef, key: Option) -> Self { @@ -116,7 +116,7 @@ where impl From> for VComp where - COMP: Component, + COMP: BaseComponent, { fn from(vchild: VChild) -> Self { VComp::new::(vchild.props, vchild.node_ref, vchild.key) @@ -127,7 +127,7 @@ impl VComp { /// Creates a new `VComp` instance. pub fn new(props: Rc, node_ref: NodeRef, key: Option) -> Self where - COMP: Component, + COMP: BaseComponent, { VComp { type_id: TypeId::of::(), @@ -183,17 +183,17 @@ trait Mountable { fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef); } -struct PropsWrapper { +struct PropsWrapper { props: Rc, } -impl PropsWrapper { +impl PropsWrapper { pub fn new(props: Rc) -> Self { Self { props } } } -impl Mountable for PropsWrapper { +impl Mountable for PropsWrapper { fn copy(&self) -> Box { let wrapper: PropsWrapper = PropsWrapper { props: Rc::clone(&self.props), @@ -225,6 +225,11 @@ impl VDiff for VComp { self.take_scope().destroy(); } + fn shift(&self, _previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) { + let scope = self.scope.as_ref().unwrap(); + scope.shift_node(next_parent.clone(), next_sibling); + } + fn apply( &mut self, parent_scope: &AnyScope, @@ -272,7 +277,7 @@ impl fmt::Debug for VComp { } } -impl fmt::Debug for VChild { +impl fmt::Debug for VChild { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("VChild<_>") } diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index 8316288402f..420a12f900f 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -291,6 +291,16 @@ impl VDiff for VList { } } + fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) { + let mut last_node_ref = next_sibling; + + for node in self.children.iter().rev() { + node.shift(previous_parent, next_parent, last_node_ref); + last_node_ref = NodeRef::default(); + last_node_ref.set(node.first_node()); + } + } + fn apply( &mut self, parent_scope: &AnyScope, diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index 6e64ef82d4f..e2af46a89cf 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -1,7 +1,7 @@ //! This module contains the implementation of abstract virtual node. -use super::{Key, VChild, VComp, VDiff, VList, VPortal, VTag, VText}; -use crate::html::{AnyScope, Component, NodeRef}; +use super::{Key, VChild, VComp, VDiff, VList, VPortal, VSuspense, VTag, VText}; +use crate::html::{AnyScope, BaseComponent, NodeRef}; use gloo::console; use std::cmp::PartialEq; use std::fmt; @@ -25,6 +25,8 @@ pub enum VNode { VPortal(VPortal), /// A holder for any `Node` (necessary for replacing node). VRef(Node), + /// A suspendible document fragment. + VSuspense(VSuspense), } impl VNode { @@ -36,6 +38,7 @@ impl VNode { VNode::VTag(vtag) => vtag.key.clone(), VNode::VText(_) => None, VNode::VPortal(vportal) => vportal.node.key(), + VNode::VSuspense(vsuspense) => vsuspense.key.clone(), } } @@ -47,6 +50,7 @@ impl VNode { VNode::VRef(_) | VNode::VText(_) => false, VNode::VTag(vtag) => vtag.key.is_some(), VNode::VPortal(vportal) => vportal.node.has_key(), + VNode::VSuspense(vsuspense) => vsuspense.key.is_some(), } } @@ -63,6 +67,7 @@ impl VNode { VNode::VList(vlist) => vlist.get(0).and_then(VNode::first_node), VNode::VRef(node) => Some(node.clone()), VNode::VPortal(vportal) => vportal.next_sibling(), + VNode::VSuspense(vsuspense) => vsuspense.first_node(), } } @@ -94,6 +99,9 @@ impl VNode { .unchecked_first_node(), VNode::VRef(node) => node.clone(), VNode::VPortal(_) => panic!("portals have no first node, they are empty inside"), + VNode::VSuspense(vsuspense) => { + vsuspense.first_node().expect("VSuspense is not mounted") + } } } @@ -130,6 +138,28 @@ impl VDiff for VNode { } } VNode::VPortal(ref mut vportal) => vportal.detach(parent), + VNode::VSuspense(ref mut vsuspense) => vsuspense.detach(parent), + } + } + + fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) { + match *self { + VNode::VTag(ref vtag) => vtag.shift(previous_parent, next_parent, next_sibling), + VNode::VText(ref vtext) => vtext.shift(previous_parent, next_parent, next_sibling), + VNode::VComp(ref vcomp) => vcomp.shift(previous_parent, next_parent, next_sibling), + VNode::VList(ref vlist) => vlist.shift(previous_parent, next_parent, next_sibling), + VNode::VRef(ref node) => { + previous_parent.remove_child(node).unwrap(); + next_parent + .insert_before(node, next_sibling.get().as_ref()) + .unwrap(); + } + VNode::VPortal(ref vportal) => { + vportal.shift(previous_parent, next_parent, next_sibling) + } + VNode::VSuspense(ref vsuspense) => { + vsuspense.shift(previous_parent, next_parent, next_sibling) + } } } @@ -166,6 +196,9 @@ impl VDiff for VNode { VNode::VPortal(ref mut vportal) => { vportal.apply(parent_scope, parent, next_sibling, ancestor) } + VNode::VSuspense(ref mut vsuspense) => { + vsuspense.apply(parent_scope, parent, next_sibling, ancestor) + } } } } @@ -204,9 +237,16 @@ impl From for VNode { } } +impl From for VNode { + #[inline] + fn from(vsuspense: VSuspense) -> Self { + VNode::VSuspense(vsuspense) + } +} + impl From> for VNode where - COMP: Component, + COMP: BaseComponent, { fn from(vchild: VChild) -> Self { VNode::VComp(VComp::from(vchild)) @@ -237,6 +277,7 @@ impl fmt::Debug for VNode { VNode::VList(ref vlist) => vlist.fmt(f), VNode::VRef(ref vref) => write!(f, "VRef ( \"{}\" )", crate::utils::print_node(vref)), VNode::VPortal(ref vportal) => vportal.fmt(f), + VNode::VSuspense(ref vsuspense) => vsuspense.fmt(f), } } } diff --git a/packages/yew/src/virtual_dom/vportal.rs b/packages/yew/src/virtual_dom/vportal.rs index f4dc14b0908..87243991599 100644 --- a/packages/yew/src/virtual_dom/vportal.rs +++ b/packages/yew/src/virtual_dom/vportal.rs @@ -22,6 +22,10 @@ impl VDiff for VPortal { self.sibling_ref.set(None); } + fn shift(&self, _previous_parent: &Element, _next_parent: &Element, _next_sibling: NodeRef) { + // portals have nothing in it's original place of DOM, we also do nothing. + } + fn apply( &mut self, parent_scope: &AnyScope, diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs new file mode 100644 index 00000000000..5cf6d177510 --- /dev/null +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -0,0 +1,147 @@ +use super::{Key, VDiff, VNode}; +use crate::html::{AnyScope, NodeRef}; +use web_sys::{Element, Node}; + +/// This struct represents a suspendable DOM fragment. +#[derive(Clone, Debug, PartialEq)] +pub struct VSuspense { + /// Child nodes. + children: Box, + + /// Fallback nodes when suspended. + fallback: Box, + + /// The element to attach to when children is not attached to DOM + detached_parent: Element, + + /// Whether the current status is suspended. + suspended: bool, + + /// The Key. + pub(crate) key: Option, +} + +impl VSuspense { + pub(crate) fn new( + children: VNode, + fallback: VNode, + detached_parent: Element, + suspended: bool, + key: Option, + ) -> Self { + Self { + children: children.into(), + fallback: fallback.into(), + detached_parent, + suspended, + key, + } + } + + pub(crate) fn first_node(&self) -> Option { + if self.suspended { + self.fallback.first_node() + } else { + self.children.first_node() + } + } +} + +impl VDiff for VSuspense { + fn detach(&mut self, parent: &Element) { + if self.suspended { + self.fallback.detach(parent); + self.children.detach(&self.detached_parent); + } else { + self.children.detach(parent); + } + } + + fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) { + if self.suspended { + self.fallback + .shift(previous_parent, next_parent, next_sibling); + } else { + self.children + .shift(previous_parent, next_parent, next_sibling); + } + } + + fn apply( + &mut self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ancestor: Option, + ) -> NodeRef { + let (already_suspended, children_ancestor, fallback_ancestor) = match ancestor { + Some(VNode::VSuspense(mut m)) => { + // We only preserve the child state if they are the same suspense. + if m.key != self.key || self.detached_parent != m.detached_parent { + m.detach(parent); + + (false, None, None) + } else { + (m.suspended, Some(*m.children), Some(*m.fallback)) + } + } + Some(mut m) => { + m.detach(parent); + (false, None, None) + } + None => (false, None, None), + }; + + // When it's suspended, we render children into an element that is detached from the dom + // tree while rendering fallback UI into the original place where children resides in. + match (self.suspended, already_suspended) { + (true, true) => { + self.children.apply( + parent_scope, + &self.detached_parent, + NodeRef::default(), + children_ancestor, + ); + + self.fallback + .apply(parent_scope, parent, next_sibling, fallback_ancestor) + } + + (false, false) => { + self.children + .apply(parent_scope, parent, next_sibling, children_ancestor) + } + + (true, false) => { + children_ancestor.as_ref().unwrap().shift( + parent, + &self.detached_parent, + NodeRef::default(), + ); + + self.children.apply( + parent_scope, + &self.detached_parent, + NodeRef::default(), + children_ancestor, + ); + + // first render of fallback, ancestor needs to be None. + self.fallback + .apply(parent_scope, parent, next_sibling, None) + } + + (false, true) => { + fallback_ancestor.unwrap().detach(parent); + + children_ancestor.as_ref().unwrap().shift( + &self.detached_parent, + parent, + next_sibling.clone(), + ); + self.children + .apply(parent_scope, parent, next_sibling, children_ancestor) + } + } + } +} diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index 2c563f4acb7..c69260ab8f7 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -493,6 +493,18 @@ impl VDiff for VTag { } } + fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) { + let node = self + .reference + .as_ref() + .expect("tried to shift not rendered VTag from DOM"); + + previous_parent.remove_child(node).unwrap(); + next_parent + .insert_before(node, next_sibling.get().as_ref()) + .unwrap(); + } + /// Renders virtual tag over DOM [Element], but it also compares this with an ancestor [VTag] /// to compute what to patch in the actual DOM nodes. fn apply( diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs index b9e43fae4c2..9b67aa665bb 100644 --- a/packages/yew/src/virtual_dom/vtext.rs +++ b/packages/yew/src/virtual_dom/vtext.rs @@ -54,6 +54,18 @@ impl VDiff for VText { } } + fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) { + let node = self + .reference + .as_ref() + .expect("tried to shift not rendered VTag from DOM"); + + previous_parent.remove_child(node).unwrap(); + next_parent + .insert_before(node, next_sibling.get().as_ref()) + .unwrap(); + } + /// Renders virtual node over existing `TextNode`, but only if value of text has changed. fn apply( &mut self, diff --git a/packages/yew/tests/mod.rs b/packages/yew/tests/mod.rs index ef98943a984..a8a7adbd897 100644 --- a/packages/yew/tests/mod.rs +++ b/packages/yew/tests/mod.rs @@ -3,7 +3,7 @@ mod common; use common::obtain_result; use wasm_bindgen_test::*; use yew::functional::{FunctionComponent, FunctionProvider}; -use yew::{html, Html, Properties}; +use yew::{html, HtmlResult, Properties}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -17,13 +17,13 @@ fn props_are_passed() { impl FunctionProvider for PropsPassedFunction { type TProps = PropsPassedFunctionProps; - fn run(props: &Self::TProps) -> Html { + fn run(props: &Self::TProps) -> HtmlResult { assert_eq!(&props.value, "props"); - return html! { + return Ok(html! {
{"done"}
- }; + }); } } type PropsComponent = FunctionComponent; diff --git a/packages/yew/tests/suspense.rs b/packages/yew/tests/suspense.rs new file mode 100644 index 00000000000..881c21c4785 --- /dev/null +++ b/packages/yew/tests/suspense.rs @@ -0,0 +1,401 @@ +mod common; + +use common::obtain_result; +use wasm_bindgen_test::*; +use yew::prelude::*; + +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + +use std::rc::Rc; + +use gloo::timers::future::TimeoutFuture; +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::spawn_local; +use web_sys::{HtmlElement, HtmlTextAreaElement}; +use yew::suspense::{Suspension, SuspensionResult}; + +#[wasm_bindgen_test] +async fn suspense_works() { + #[derive(PartialEq)] + pub struct SleepState { + s: Suspension, + } + + impl SleepState { + fn new() -> Self { + let (s, handle) = Suspension::new(); + + spawn_local(async move { + TimeoutFuture::new(50).await; + + handle.resume(); + }); + + Self { s } + } + } + + impl Reducible for SleepState { + type Action = (); + + fn reduce(self: Rc, _action: Self::Action) -> Rc { + Self::new().into() + } + } + + pub fn use_sleep() -> SuspensionResult> { + let sleep_state = use_reducer(SleepState::new); + + if sleep_state.s.resumed() { + Ok(Rc::new(move || sleep_state.dispatch(()))) + } else { + Err(sleep_state.s.clone()) + } + } + + #[function_component(Content)] + fn content() -> HtmlResult { + let resleep = use_sleep()?; + + let value = use_state(|| 0); + + let on_increment = { + let value = value.clone(); + + Callback::from(move |_: MouseEvent| { + value.set(*value + 1); + }) + }; + + let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())()); + + Ok(html! { +
+
{*value}
+ +
+ +
+
+ }) + } + + #[function_component(App)] + fn app() -> Html { + let fallback = html! {
{"wait..."}
}; + + html! { +
+ + + +
+ } + } + + yew::start_app_in_element::(gloo_utils::document().get_element_by_id("output").unwrap()); + + TimeoutFuture::new(10).await; + let result = obtain_result(); + assert_eq!(result.as_str(), "
wait...
"); + + TimeoutFuture::new(50).await; + + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
0
"# + ); + + TimeoutFuture::new(10).await; + + gloo_utils::document() + .query_selector(".increase") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap() + .click(); + + gloo_utils::document() + .query_selector(".increase") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap() + .click(); + + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
2
"# + ); + + gloo_utils::document() + .query_selector(".take-a-break") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap() + .click(); + + TimeoutFuture::new(10).await; + let result = obtain_result(); + assert_eq!(result.as_str(), "
wait...
"); + + TimeoutFuture::new(50).await; + + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
2
"# + ); +} + +#[wasm_bindgen_test] +async fn suspense_not_suspended_at_start() { + #[derive(PartialEq)] + pub struct SleepState { + s: Option, + } + + impl SleepState { + fn new() -> Self { + Self { s: None } + } + } + + impl Reducible for SleepState { + type Action = (); + + fn reduce(self: Rc, _action: Self::Action) -> Rc { + let (s, handle) = Suspension::new(); + + spawn_local(async move { + TimeoutFuture::new(50).await; + + handle.resume(); + }); + + Self { s: Some(s) }.into() + } + } + + pub fn use_sleep() -> SuspensionResult> { + let sleep_state = use_reducer(SleepState::new); + + let s = match sleep_state.s.clone() { + Some(m) => m, + None => return Ok(Rc::new(move || sleep_state.dispatch(()))), + }; + + if s.resumed() { + Ok(Rc::new(move || sleep_state.dispatch(()))) + } else { + Err(s) + } + } + + #[function_component(Content)] + fn content() -> HtmlResult { + let resleep = use_sleep()?; + + let value = use_state(|| "I am writing a long story...".to_string()); + + let on_text_input = { + let value = value.clone(); + + Callback::from(move |e: InputEvent| { + let input: HtmlTextAreaElement = e.target_unchecked_into(); + + value.set(input.value()); + }) + }; + + let on_take_a_break = Callback::from(move |_| (resleep.clone())()); + + Ok(html! { +
+ +
+ +
+
+ }) + } + + #[function_component(App)] + fn app() -> Html { + let fallback = html! {
{"wait..."}
}; + + html! { +
+ + + +
+ } + } + + yew::start_app_in_element::(gloo_utils::document().get_element_by_id("output").unwrap()); + + TimeoutFuture::new(10).await; + + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
"# + ); + gloo_utils::document() + .query_selector(".take-a-break") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap() + .click(); + + TimeoutFuture::new(10).await; + let result = obtain_result(); + assert_eq!(result.as_str(), "
wait...
"); + + TimeoutFuture::new(50).await; + + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
"# + ); +} + +#[wasm_bindgen_test] +async fn suspense_nested_suspense_works() { + #[derive(PartialEq)] + pub struct SleepState { + s: Suspension, + } + + impl SleepState { + fn new() -> Self { + let (s, handle) = Suspension::new(); + + spawn_local(async move { + TimeoutFuture::new(50).await; + + handle.resume(); + }); + + Self { s } + } + } + + impl Reducible for SleepState { + type Action = (); + + fn reduce(self: Rc, _action: Self::Action) -> Rc { + Self::new().into() + } + } + + pub fn use_sleep() -> SuspensionResult> { + let sleep_state = use_reducer(SleepState::new); + + if sleep_state.s.resumed() { + Ok(Rc::new(move || sleep_state.dispatch(()))) + } else { + Err(sleep_state.s.clone()) + } + } + + #[function_component(InnerContent)] + fn inner_content() -> HtmlResult { + let resleep = use_sleep()?; + + let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())()); + + Ok(html! { +
+
+ +
+
+ }) + } + + #[function_component(Content)] + fn content() -> HtmlResult { + let resleep = use_sleep()?; + + let fallback = html! {
{"wait...(inner)"}
}; + + let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())()); + + Ok(html! { +
+
+ +
+ + + +
+ }) + } + + #[function_component(App)] + fn app() -> Html { + let fallback = html! {
{"wait...(outer)"}
}; + + html! { +
+ + + +
+ } + } + + yew::start_app_in_element::(gloo_utils::document().get_element_by_id("output").unwrap()); + + TimeoutFuture::new(10).await; + let result = obtain_result(); + assert_eq!(result.as_str(), "
wait...(outer)
"); + + TimeoutFuture::new(50).await; + + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
wait...(inner)
"# + ); + + TimeoutFuture::new(50).await; + + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
"# + ); + + gloo_utils::document() + .query_selector(".take-a-break2") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap() + .click(); + + TimeoutFuture::new(10).await; + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
wait...(inner)
"# + ); + + TimeoutFuture::new(50).await; + + let result = obtain_result(); + assert_eq!( + result.as_str(), + r#"
"# + ); +} diff --git a/packages/yew/tests/use_context.rs b/packages/yew/tests/use_context.rs index 23e76df07d2..4fffc969e94 100644 --- a/packages/yew/tests/use_context.rs +++ b/packages/yew/tests/use_context.rs @@ -6,7 +6,7 @@ use wasm_bindgen_test::*; use yew::functional::{ use_context, use_effect, use_mut_ref, use_state, FunctionComponent, FunctionProvider, }; -use yew::{html, Children, ContextProvider, Html, Properties}; +use yew::{html, Children, ContextProvider, HtmlResult, Properties}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -23,24 +23,24 @@ fn use_context_scoping_works() { impl FunctionProvider for ExpectNoContextFunction { type TProps = (); - fn run(_props: &Self::TProps) -> Html { + fn run(_props: &Self::TProps) -> HtmlResult { if use_context::().is_some() { console_log!( "Context should be None here, but was {:?}!", use_context::().unwrap() ); }; - return html! { + Ok(html! {
- }; + }) } } impl FunctionProvider for UseContextFunctionOuter { type TProps = (); - fn run(_props: &Self::TProps) -> Html { + fn run(_props: &Self::TProps) -> HtmlResult { type ExampleContextProvider = ContextProvider; - return html! { + Ok(html! {
{"ignored"}
@@ -58,17 +58,17 @@ fn use_context_scoping_works() {
- }; + }) } } impl FunctionProvider for UseContextFunctionInner { type TProps = (); - fn run(_props: &Self::TProps) -> Html { + fn run(_props: &Self::TProps) -> HtmlResult { let context = use_context::(); - return html! { + Ok(html! {
{ &context.unwrap().0 }
- }; + }) } } @@ -90,11 +90,11 @@ fn use_context_works_with_multiple_types() { impl FunctionProvider for Test1Function { type TProps = (); - fn run(_props: &Self::TProps) -> Html { + fn run(_props: &Self::TProps) -> HtmlResult { assert_eq!(use_context::(), Some(ContextA(2))); assert_eq!(use_context::(), Some(ContextB(1))); - return html! {}; + Ok(html! {}) } } type Test1 = FunctionComponent; @@ -103,11 +103,11 @@ fn use_context_works_with_multiple_types() { impl FunctionProvider for Test2Function { type TProps = (); - fn run(_props: &Self::TProps) -> Html { + fn run(_props: &Self::TProps) -> HtmlResult { assert_eq!(use_context::(), Some(ContextA(0))); assert_eq!(use_context::(), Some(ContextB(1))); - return html! {}; + Ok(html! {}) } } type Test2 = FunctionComponent; @@ -116,11 +116,11 @@ fn use_context_works_with_multiple_types() { impl FunctionProvider for Test3Function { type TProps = (); - fn run(_props: &Self::TProps) -> Html { + fn run(_props: &Self::TProps) -> HtmlResult { assert_eq!(use_context::(), Some(ContextA(0))); assert_eq!(use_context::(), None); - return html! {}; + Ok(html! {}) } } type Test3 = FunctionComponent; @@ -129,11 +129,11 @@ fn use_context_works_with_multiple_types() { impl FunctionProvider for Test4Function { type TProps = (); - fn run(_props: &Self::TProps) -> Html { + fn run(_props: &Self::TProps) -> HtmlResult { assert_eq!(use_context::(), None); assert_eq!(use_context::(), None); - return html! {}; + Ok(html! {}) } } type Test4 = FunctionComponent; @@ -142,11 +142,11 @@ fn use_context_works_with_multiple_types() { impl FunctionProvider for TestFunction { type TProps = (); - fn run(_props: &Self::TProps) -> Html { + fn run(_props: &Self::TProps) -> HtmlResult { type ContextAProvider = ContextProvider; type ContextBProvider = ContextProvider; - return html! { + Ok(html! {
@@ -159,7 +159,7 @@ fn use_context_works_with_multiple_types() {
- }; + }) } } type TestComponent = FunctionComponent; @@ -184,17 +184,17 @@ fn use_context_update_works() { impl FunctionProvider for RenderCounterFunction { type TProps = RenderCounterProps; - fn run(props: &Self::TProps) -> Html { + fn run(props: &Self::TProps) -> HtmlResult { let counter = use_mut_ref(|| 0); *counter.borrow_mut() += 1; - return html! { + Ok(html! { <>
{ format!("total: {}", counter.borrow()) }
{ props.children.clone() } - }; + }) } } type RenderCounter = FunctionComponent; @@ -209,20 +209,20 @@ fn use_context_update_works() { impl FunctionProvider for ContextOutletFunction { type TProps = ContextOutletProps; - fn run(props: &Self::TProps) -> Html { + fn run(props: &Self::TProps) -> HtmlResult { let counter = use_mut_ref(|| 0); *counter.borrow_mut() += 1; let ctx = use_context::>().expect("context not passed down"); - return html! { + Ok(html! { <>
{ format!("magic: {}\n", props.magic) }
{ format!("current: {}, total: {}", ctx.0, counter.borrow()) }
- }; + }) } } type ContextOutlet = FunctionComponent; @@ -231,7 +231,7 @@ fn use_context_update_works() { impl FunctionProvider for TestFunction { type TProps = (); - fn run(_props: &Self::TProps) -> Html { + fn run(_props: &Self::TProps) -> HtmlResult { type MyContextProvider = ContextProvider>; let ctx = use_state(|| MyContext("hello".into())); @@ -263,14 +263,14 @@ fn use_context_update_works() { || {} }); } - return html! { + Ok(html! { - }; + }) } } type TestComponent = FunctionComponent; diff --git a/packages/yew/tests/use_effect.rs b/packages/yew/tests/use_effect.rs index 301a210a614..bf064a87064 100644 --- a/packages/yew/tests/use_effect.rs +++ b/packages/yew/tests/use_effect.rs @@ -7,7 +7,7 @@ use wasm_bindgen_test::*; use yew::functional::{ use_effect_with_deps, use_mut_ref, use_state, FunctionComponent, FunctionProvider, }; -use yew::{html, Html, Properties}; +use yew::{html, HtmlResult, Properties}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -39,7 +39,7 @@ fn use_effect_destroys_on_component_drop() { impl FunctionProvider for UseEffectFunction { type TProps = FunctionProps; - fn run(props: &Self::TProps) -> Html { + fn run(props: &Self::TProps) -> HtmlResult { let effect_called = props.effect_called.clone(); let destroy_called = props.destroy_called.clone(); use_effect_with_deps( @@ -50,23 +50,23 @@ fn use_effect_destroys_on_component_drop() { }, (), ); - return html! {}; + Ok(html! {}) } } impl FunctionProvider for UseEffectWrapper { type TProps = WrapperProps; - fn run(props: &Self::TProps) -> Html { + fn run(props: &Self::TProps) -> HtmlResult { let show = use_state(|| true); if *show { let effect_called: Rc = { Rc::new(move || show.set(false)) }; - html! { + Ok(html! { - } + }) } else { - html! { + Ok(html! {
{ "EMPTY" }
- } + }) } } } @@ -87,7 +87,7 @@ fn use_effect_works_many_times() { impl FunctionProvider for UseEffectFunction { type TProps = (); - fn run(_: &Self::TProps) -> Html { + fn run(_: &Self::TProps) -> HtmlResult { let counter = use_state(|| 0); let counter_clone = counter.clone(); @@ -101,13 +101,13 @@ fn use_effect_works_many_times() { *counter, ); - return html! { + Ok(html! {
{ "The test result is" }
{ *counter }
{ "\n" }
- }; + }) } } @@ -125,7 +125,7 @@ fn use_effect_works_once() { impl FunctionProvider for UseEffectFunction { type TProps = (); - fn run(_: &Self::TProps) -> Html { + fn run(_: &Self::TProps) -> HtmlResult { let counter = use_state(|| 0); let counter_clone = counter.clone(); @@ -137,13 +137,13 @@ fn use_effect_works_once() { (), ); - return html! { + Ok(html! {
{ "The test result is" }
{ *counter }
{ "\n" }
- }; + }) } } type UseEffectComponent = FunctionComponent; @@ -160,7 +160,7 @@ fn use_effect_refires_on_dependency_change() { impl FunctionProvider for UseEffectFunction { type TProps = (); - fn run(_: &Self::TProps) -> Html { + fn run(_: &Self::TProps) -> HtmlResult { let number_ref = use_mut_ref(|| 0); let number_ref_c = number_ref.clone(); let number_ref2 = use_mut_ref(|| 0); @@ -185,13 +185,13 @@ fn use_effect_refires_on_dependency_change() { }, arg, ); - return html! { + Ok(html! {
{"The test result is"}
{*number_ref.borrow_mut().deref_mut()}{*number_ref2.borrow_mut().deref_mut()}
{"\n"}
- }; + }) } } type UseEffectComponent = FunctionComponent; diff --git a/packages/yew/tests/use_reducer.rs b/packages/yew/tests/use_reducer.rs index ff54ffe83be..de81203edd6 100644 --- a/packages/yew/tests/use_reducer.rs +++ b/packages/yew/tests/use_reducer.rs @@ -34,7 +34,7 @@ fn use_reducer_works() { struct UseReducerFunction {} impl FunctionProvider for UseReducerFunction { type TProps = (); - fn run(_: &Self::TProps) -> Html { + fn run(_: &Self::TProps) -> HtmlResult { let counter = use_reducer(|| CounterState { counter: 10 }); let counter_clone = counter.clone(); @@ -45,13 +45,13 @@ fn use_reducer_works() { }, (), ); - return html! { + Ok(html! {
{"The test result is"}
{counter.counter}
{"\n"}
- }; + }) } } type UseReducerComponent = FunctionComponent; @@ -83,7 +83,7 @@ fn use_reducer_eq_works() { struct UseReducerFunction {} impl FunctionProvider for UseReducerFunction { type TProps = (); - fn run(_: &Self::TProps) -> Html { + fn run(_: &Self::TProps) -> HtmlResult { let content = use_reducer_eq(|| ContentState { content: HashSet::default(), }); @@ -104,7 +104,7 @@ fn use_reducer_eq_works() { let add_content_b = Callback::from(move |_| content.dispatch("B".to_string())); - return html! { + Ok(html! { <>
{"This component has been rendered: "}{render_count}{" Time(s)."} @@ -112,7 +112,7 @@ fn use_reducer_eq_works() { - }; + }) } } type UseReducerComponent = FunctionComponent; diff --git a/packages/yew/tests/use_ref.rs b/packages/yew/tests/use_ref.rs index 80c86c76394..c27a0faea61 100644 --- a/packages/yew/tests/use_ref.rs +++ b/packages/yew/tests/use_ref.rs @@ -4,7 +4,7 @@ use common::obtain_result; use std::ops::DerefMut; use wasm_bindgen_test::*; use yew::functional::{use_mut_ref, use_state, FunctionComponent, FunctionProvider}; -use yew::{html, Html}; +use yew::{html, HtmlResult}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -14,20 +14,20 @@ fn use_ref_works() { impl FunctionProvider for UseRefFunction { type TProps = (); - fn run(_: &Self::TProps) -> Html { + fn run(_: &Self::TProps) -> HtmlResult { let ref_example = use_mut_ref(|| 0); *ref_example.borrow_mut().deref_mut() += 1; let counter = use_state(|| 0); if *counter < 5 { counter.set(*counter + 1) } - return html! { + Ok(html! {
{"The test output is: "}
{*ref_example.borrow_mut().deref_mut() > 4}
{"\n"}
- }; + }) } } type UseRefComponent = FunctionComponent; diff --git a/packages/yew/tests/use_state.rs b/packages/yew/tests/use_state.rs index 832cceaef85..78146c6ca4b 100644 --- a/packages/yew/tests/use_state.rs +++ b/packages/yew/tests/use_state.rs @@ -5,7 +5,7 @@ use wasm_bindgen_test::*; use yew::functional::{ use_effect_with_deps, use_state, use_state_eq, FunctionComponent, FunctionProvider, }; -use yew::{html, Html}; +use yew::{html, HtmlResult}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -15,18 +15,18 @@ fn use_state_works() { impl FunctionProvider for UseStateFunction { type TProps = (); - fn run(_: &Self::TProps) -> Html { + fn run(_: &Self::TProps) -> HtmlResult { let counter = use_state(|| 0); if *counter < 5 { counter.set(*counter + 1) } - return html! { + return Ok(html! {
{"Test Output: "}
{*counter}
{"\n"}
- }; + }); } } type UseComponent = FunctionComponent; @@ -43,7 +43,7 @@ fn multiple_use_state_setters() { impl FunctionProvider for UseStateFunction { type TProps = (); - fn run(_: &Self::TProps) -> Html { + fn run(_: &Self::TProps) -> HtmlResult { let counter = use_state(|| 0); let counter_clone = counter.clone(); use_effect_with_deps( @@ -64,14 +64,14 @@ fn multiple_use_state_setters() { } }; another_scope(); - return html! { + Ok(html! {
{ "Test Output: " } // expected output
{ *counter }
{ "\n" }
- }; + }) } } type UseComponent = FunctionComponent; @@ -92,18 +92,18 @@ fn use_state_eq_works() { impl FunctionProvider for UseStateFunction { type TProps = (); - fn run(_: &Self::TProps) -> Html { + fn run(_: &Self::TProps) -> HtmlResult { RENDER_COUNT.fetch_add(1, Ordering::Relaxed); let counter = use_state_eq(|| 0); counter.set(1); - return html! { + Ok(html! {
{"Test Output: "}
{*counter}
{"\n"}
- }; + }) } } type UseComponent = FunctionComponent; diff --git a/website/docs/advanced-topics/portals.mdx b/website/docs/advanced-topics/portals.mdx index 69bd3e1a11d..b7a449ab8b1 100644 --- a/website/docs/advanced-topics/portals.mdx +++ b/website/docs/advanced-topics/portals.mdx @@ -16,7 +16,7 @@ Typical uses of portals can include modal dialogs and hovercards, as well as mor Note that `yew::create_portal` is a rather low-level building block, on which other components should be built that provide the interface for your specific use case. As an example, here is a simple modal dialogue that renders its `children` into an element outside `yew`'s control, identified by the `id="modal_host"`. ```rust -use yew::{html, create_portal, function_component, Children, Properties}; +use yew::{html, create_portal, function_component, Children, Properties, Html}; #[derive(Properties, PartialEq)] pub struct ModalProps { diff --git a/website/docs/concepts/function-components/attribute.mdx b/website/docs/concepts/function-components/attribute.mdx index ae0da06fe1d..02edccd0d13 100644 --- a/website/docs/concepts/function-components/attribute.mdx +++ b/website/docs/concepts/function-components/attribute.mdx @@ -54,7 +54,7 @@ html! { ```rust -use yew::{function_component, html, Properties}; +use yew::{function_component, html, Properties, Html}; #[derive(Properties, PartialEq)] pub struct RenderedAtProps { @@ -76,7 +76,7 @@ pub fn RenderedAt(props: &RenderedAtProps) -> Html { ```rust -use yew::{function_component, html, use_state, Callback}; +use yew::{function_component, html, use_state, Callback, Html}; #[function_component] fn App() -> Html { @@ -108,7 +108,7 @@ The `#[function_component(_)]` attribute also works with generic functions for c ```rust title=my_generic_component.rs use std::fmt::Display; -use yew::{function_component, html, Properties}; +use yew::{function_component, html, Properties, Html}; #[derive(Properties, PartialEq)] pub struct Props diff --git a/website/docs/concepts/function-components/introduction.mdx b/website/docs/concepts/function-components/introduction.mdx index 29acf8326a3..7aab0326701 100644 --- a/website/docs/concepts/function-components/introduction.mdx +++ b/website/docs/concepts/function-components/introduction.mdx @@ -16,7 +16,7 @@ implement the `Component` trait. The easiest way to create a function component is to add the [`#[function_component]`](./../function-components/attribute.mdx) attribute to a function. ```rust -use yew::{function_component, html}; +use yew::{function_component, html, Html}; #[function_component] fn HelloWorld() -> Html { diff --git a/website/docs/concepts/function-components/pre-defined-hooks.mdx b/website/docs/concepts/function-components/pre-defined-hooks.mdx index 5cb71a76452..0c800dfbce4 100644 --- a/website/docs/concepts/function-components/pre-defined-hooks.mdx +++ b/website/docs/concepts/function-components/pre-defined-hooks.mdx @@ -24,7 +24,7 @@ re-render when the state changes. ### Example ```rust -use yew::{Callback, function_component, html, use_state}; +use yew::{Callback, function_component, html, use_state, Html}; #[function_component(UseState)] fn state() -> Html { @@ -75,7 +75,7 @@ If you need a mutable reference, consider using [`use_mut_ref`](#use_mut_ref). If you need the component to be re-rendered on state change, consider using [`use_state`](#use_state). ```rust -use yew::{function_component, html, use_ref, use_state, Callback}; +use yew::{function_component, html, use_ref, use_state, Callback, Html}; #[function_component(UseRef)] fn ref_hook() -> Html { @@ -104,6 +104,7 @@ use yew::{ events::Event, function_component, html, use_mut_ref, use_state, Callback, TargetCast, + Html, }; #[function_component(UseMutRef)] @@ -152,7 +153,7 @@ DOM. use web_sys::HtmlInputElement; use yew::{ function_component, functional::*, html, - NodeRef + NodeRef, Html }; #[function_component(UseRef)] @@ -296,7 +297,7 @@ The destructor can be used to clean up the effects introduced and it can take ow ### Example ```rust -use yew::{Callback, function_component, html, use_effect, use_state}; +use yew::{Callback, function_component, html, use_effect, use_state, Html}; #[function_component(UseEffect)] fn effect() -> Html { @@ -348,7 +349,7 @@ use_effect_with_deps( ### Example ```rust -use yew::{ContextProvider, function_component, html, use_context, use_state}; +use yew::{ContextProvider, function_component, html, use_context, use_state, Html}; /// App theme diff --git a/website/docs/concepts/suspense.md b/website/docs/concepts/suspense.md new file mode 100644 index 00000000000..a8ac666a65d --- /dev/null +++ b/website/docs/concepts/suspense.md @@ -0,0 +1,180 @@ +--- +title: "Suspense" +description: "Suspense for data fetching" +--- + +Suspense is a way to suspend component rendering whilst waiting a task +to complete and a fallback (placeholder) UI is shown in the meanwhile. + +It can be used to fetch data from server, wait for tasks to be completed +by an agent, or perform other background asynchronous task. + +Before suspense, data fetching usually happens after (Fetch-on-render) or before +component rendering (Fetch-then-render). + +### Render-as-You-Fetch + +Suspense enables a new approach that allows components to initiate data request +during the rendering process. When a component initiates a data request, +the rendering process will become suspended and a fallback UI will be +shown until the request is completed. + +The recommended way to use suspense is with hooks. + +```rust ,ignore +use yew::prelude::*; + +#[function_component(Content)] +fn content() -> HtmlResult { + let user = use_user()?; + + Ok(html! {
{"Hello, "}{&user.name}
}) +} + +#[function_component(App)] +fn app() -> Html { + let fallback = html! {
{"Loading..."}
}; + + html! { + + + + } +} +``` + +In the above example, the `use_user` hook will suspend the component +rendering while user information is loading and a `Loading...` placeholder will +be shown until `user` is loaded. + +To define a hook that suspends a component rendering, it needs to return +a `SuspensionResult`. When the component needs to be suspended, the +hook should return a `Err(Suspension)` and users should unwrap it with +`?` in which it will be converted into `Html`. + +```rust ,ignore +use yew::prelude::*; +use yew::suspense::{Suspension, SuspensionResult}; + +struct User { + name: String, +} + +fn use_user() -> SuspensionResult { + match load_user() { + // If a user is loaded, then we return it as Ok(user). + Some(m) => Ok(m), + None => { + // When user is still loading, then we create a `Suspension` + // and call `SuspensionHandle::resume` when data loading + // completes, the component will be re-rendered + // automatically. + let (s, handle) = Suspension::new(); + on_load_user_complete(move || {handle.resume();}); + Err(s) + }, + } +} +``` + +# Complete Example + +```rust +use yew::prelude::*; +use yew::suspense::{Suspension, SuspensionResult}; + +#[derive(Debug)] +struct User { + name: String, +} + +fn load_user() -> Option { + todo!() // implementation omitted. +} + +fn on_load_user_complete(_fn: F) { + todo!() // implementation omitted. +} + +fn use_user() -> SuspensionResult { + match load_user() { + // If a user is loaded, then we return it as Ok(user). + Some(m) => Ok(m), + None => { + // When user is still loading, then we create a `Suspension` + // and call `SuspensionHandle::resume` when data loading + // completes, the component will be re-rendered + // automatically. + let (s, handle) = Suspension::new(); + on_load_user_complete(move || {handle.resume();}); + Err(s) + }, + } +} + +#[function_component(Content)] +fn content() -> HtmlResult { + let user = use_user()?; + + Ok(html! {
{"Hello, "}{&user.name}
}) +} + +#[function_component(App)] +fn app() -> Html { + let fallback = html! {
{"Loading..."}
}; + + html! { + + + + } +} +``` + + +### Use Suspense in Struct Components + +It's not possible to suspend a struct component directly. However, you +can use a function component as a Higher-Order-Component to +achieve suspense-based data fetching. + +```rust ,ignore +use yew::prelude::*; + +#[function_component(WithUser)] +fn with_user() -> HtmlResult +where T: BaseComponent +{ + let user = use_user()?; + + Ok(html! {}) +} + +#[derive(Debug, PartialEq, Properties)] +pub struct UserContentProps { + pub user: User, +} + +pub struct BaseUserContent; + +impl Component for BaseUserContent { + type Properties = UserContentProps; + type Message = (); + + fn create(ctx: &Context) -> Self { + Self + } + + fn view(&self, ctx: &Context) -> Html { + let name = ctx.props().user.name; + + html! {
{"Hello, "}{name}{"!"}
} + } +} + +pub type UserContent = WithUser; +``` + +## Relevant examples + +- [Suspense](https://github.com/yewstack/yew/tree/master/examples/suspense) diff --git a/website/sidebars.js b/website/sidebars.js index 17078dd6f22..0f5f8f5df58 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -13,122 +13,123 @@ module.exports = { // By default, Docusaurus generates a sidebar from the docs folder structure // conceptsSidebar: [{type: 'autogenerated', dirName: '.'}], - // But you can create a sidebar manually - sidebar: [ + // But you can create a sidebar manually + sidebar: [ + { + type: "category", + label: "Getting Started", + items: [ { - type: 'category', - label: 'Getting Started', - items: [ - { - type: 'category', - label: 'Project Setup', - items: [ - 'getting-started/project-setup/introduction', - 'getting-started/project-setup/using-trunk', - 'getting-started/project-setup/using-wasm-pack', - ] - }, - "getting-started/build-a-sample-app", - "getting-started/examples", - "getting-started/starter-templates", - ], + type: "category", + label: "Project Setup", + items: [ + "getting-started/project-setup/introduction", + "getting-started/project-setup/using-trunk", + "getting-started/project-setup/using-wasm-pack", + ], }, + "getting-started/build-a-sample-app", + "getting-started/examples", + "getting-started/starter-templates", + ], + }, + { + type: "category", + label: "Concepts", + items: [ { - type: "category", - label: "Concepts", - items: [ - { - type: "category", - label: "wasm-bindgen", - items: [ - "concepts/wasm-bindgen/introduction", - "concepts/wasm-bindgen/web-sys", - ] - }, - { - type: "category", - label: "Components", - items: [ - "concepts/components/introduction", - "concepts/components/callbacks", - "concepts/components/scope", - "concepts/components/properties", - "concepts/components/children", - "concepts/components/refs" - ], - }, - { - type: "category", - label: "HTML", - items: [ - "concepts/html/introduction", - "concepts/html/components", - "concepts/html/elements", - "concepts/html/events", - "concepts/html/classes", - "concepts/html/fragments", - "concepts/html/lists", - "concepts/html/literals-and-expressions" - ] - }, - { - type: "category", - label: "Function Components", - items: [ - "concepts/function-components/introduction", - "concepts/function-components/attribute", - "concepts/function-components/pre-defined-hooks", - "concepts/function-components/custom-hooks", - ] - }, - "concepts/agents", - "concepts/contexts", - "concepts/router", - ] + type: "category", + label: "wasm-bindgen", + items: [ + "concepts/wasm-bindgen/introduction", + "concepts/wasm-bindgen/web-sys", + ], }, { - type: 'category', - label: 'Advanced topics', - items: [ - "advanced-topics/how-it-works", - "advanced-topics/optimizations", - "advanced-topics/portals", - ] + type: "category", + label: "Components", + items: [ + "concepts/components/introduction", + "concepts/components/callbacks", + "concepts/components/scope", + "concepts/components/properties", + "concepts/components/children", + "concepts/components/refs", + ], }, { - type: 'category', - label: 'More', - items: [ - "more/debugging", - "more/development-tips", - "more/external-libs", - "more/css", - "more/testing", - "more/roadmap", - "more/wasm-build-tools" - ] + type: "category", + label: "HTML", + items: [ + "concepts/html/introduction", + "concepts/html/components", + "concepts/html/elements", + "concepts/html/events", + "concepts/html/classes", + "concepts/html/fragments", + "concepts/html/lists", + "concepts/html/literals-and-expressions", + ], }, { - type: "category", - label: "Migration guides", - items: [ - { - type: "category", - label: "yew", - items: ["migration-guides/yew/from-0_18_0-to-0_19_0"], - }, - { - type: "category", - label: "yew-agent", - items: ["migration-guides/yew-agent/from-0_0_0-to-0_1_0"], - }, - { - type: "category", - label: "yew-router", - items: ["migration-guides/yew-router/from-0_15_0-to-0_16_0"], - }, - ], + type: "category", + label: "Function Components", + items: [ + "concepts/function-components/introduction", + "concepts/function-components/attribute", + "concepts/function-components/pre-defined-hooks", + "concepts/function-components/custom-hooks", + ], }, - "tutorial" - ], + "concepts/agents", + "concepts/contexts", + "concepts/router", + "concepts/suspense", + ], + }, + { + type: "category", + label: "Advanced topics", + items: [ + "advanced-topics/how-it-works", + "advanced-topics/optimizations", + "advanced-topics/portals", + ], + }, + { + type: "category", + label: "More", + items: [ + "more/debugging", + "more/development-tips", + "more/external-libs", + "more/css", + "more/testing", + "more/roadmap", + "more/wasm-build-tools", + ], + }, + { + type: "category", + label: "Migration guides", + items: [ + { + type: "category", + label: "yew", + items: ["migration-guides/yew/from-0_18_0-to-0_19_0"], + }, + { + type: "category", + label: "yew-agent", + items: ["migration-guides/yew-agent/from-0_0_0-to-0_1_0"], + }, + { + type: "category", + label: "yew-router", + items: ["migration-guides/yew-router/from-0_15_0-to-0_16_0"], + }, + ], + }, + "tutorial", + ], };