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! {
+
+
+
+
{"Take a break!"}
+
{"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