mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
Suspense Support (#2212)
* Make Html a Result. * Fix tests. * Implement Suspense. * Schedule render when suspension is resumed. * Shift children into a detached node. * styled example. * Update wording a little bit. * Move hint to hint. * Add some tests. * Fix clippy. * Add docs. * Add to sidebar. * Fix syntax highlight. * Component -> BaseComponent. * Html -> VNode, HtmlResult = RenderResult<Html>. * Suspendible Function Component. * Add a method to create suspension from futures. * Revert extra changes. * Fix tests. * Update documentation. * Switch to custom trait to make test reliable. * Fix file permission. * Fix docs. * Remove log. * Fix file permission. * Fix component name error. * Make Suspension a future.
This commit is contained in:
parent
eb23b327d9
commit
ac3af0a9bc
@ -31,6 +31,7 @@ members = [
|
||||
"examples/two_apps",
|
||||
"examples/webgl",
|
||||
"examples/web_worker_fib",
|
||||
"examples/suspense",
|
||||
|
||||
# Tools
|
||||
"tools/changelog",
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use yew::{function_component, html};
|
||||
use yew::prelude::*;
|
||||
|
||||
#[function_component(InfoFooter)]
|
||||
pub fn info_footer() -> Html {
|
||||
|
||||
@ -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;
|
||||
|
||||
19
examples/suspense/Cargo.toml
Normal file
19
examples/suspense/Cargo.toml
Normal file
@ -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",
|
||||
]
|
||||
10
examples/suspense/README.md
Normal file
10
examples/suspense/README.md
Normal file
@ -0,0 +1,10 @@
|
||||
# Suspense Example
|
||||
|
||||
[](https://examples.yew.rs/suspense)
|
||||
|
||||
This is an example that demonstrates `<Suspense />` support.
|
||||
|
||||
## Concepts
|
||||
|
||||
This example shows that how `<Suspense />` works in Yew and how you can
|
||||
create hooks that utilises suspense.
|
||||
11
examples/suspense/index.html
Normal file
11
examples/suspense/index.html
Normal file
@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Yew Suspense Demo</title>
|
||||
|
||||
<link data-trunk rel="sass" href="index.scss" />
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
</html>
|
||||
71
examples/suspense/index.scss
Normal file
71
examples/suspense/index.scss
Normal file
@ -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);
|
||||
}
|
||||
60
examples/suspense/src/main.rs
Normal file
60
examples/suspense/src/main.rs
Normal file
@ -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! {<div class="content-area">{"Please wait 5 Seconds..."}</div>}
|
||||
}
|
||||
|
||||
#[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! {
|
||||
<div class="content-area">
|
||||
<textarea value={value.to_string()} oninput={on_text_input}></textarea>
|
||||
<div class="action-area">
|
||||
<button onclick={on_take_a_break}>{"Take a break!"}</button>
|
||||
<div class="hint">{"You can take a break at anytime"}<br />{"and your work will be preserved."}</div>
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
|
||||
#[function_component(App)]
|
||||
fn app() -> Html {
|
||||
let fallback = html! {<PleaseWait />};
|
||||
|
||||
html! {
|
||||
<div class="layout">
|
||||
<div class="content">
|
||||
<h1>{"Yew Suspense Demo"}</h1>
|
||||
<Suspense fallback={fallback}>
|
||||
<AppContent />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::start_app::<App>();
|
||||
}
|
||||
39
examples/suspense/src/use_sleep.rs
Normal file
39
examples/suspense/src/use_sleep.rs
Normal file
@ -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<Self>, _action: Self::Action) -> Rc<Self> {
|
||||
Self::new().into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
@ -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<Block>,
|
||||
props_type: Box<Type>,
|
||||
@ -15,127 +15,132 @@ pub struct FunctionComponent {
|
||||
attrs: Vec<Attribute>,
|
||||
name: Ident,
|
||||
return_type: Box<Type>,
|
||||
fn_token: Fn,
|
||||
}
|
||||
|
||||
impl Parse for FunctionComponent {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let parsed: Item = input.parse()?;
|
||||
|
||||
match parsed {
|
||||
Item::Fn(func) => {
|
||||
let ItemFn {
|
||||
attrs,
|
||||
vis,
|
||||
let func = match parsed {
|
||||
Item::Fn(m) => m,
|
||||
|
||||
item => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
item,
|
||||
"`function_component` attribute can only be applied to functions",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
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",
|
||||
));
|
||||
}
|
||||
|
||||
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,
|
||||
block,
|
||||
} = func;
|
||||
"function components must return `yew::Html` or `yew::HtmlResult`",
|
||||
))
|
||||
}
|
||||
ReturnType::Type(_, ty) => ty,
|
||||
};
|
||||
|
||||
if sig.generics.lifetimes().next().is_some() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
sig.generics,
|
||||
"function components can't have generic lifetime parameters",
|
||||
));
|
||||
}
|
||||
let mut inputs = sig.inputs.into_iter();
|
||||
let arg = inputs
|
||||
.next()
|
||||
.unwrap_or_else(|| syn::parse_quote! { _: &() });
|
||||
|
||||
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 => {
|
||||
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`",
|
||||
))
|
||||
}
|
||||
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(_) => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
arg,
|
||||
"function components can't accept a receiver",
|
||||
&ty.lifetime,
|
||||
"reference must not have a lifetime",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// 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",
|
||||
));
|
||||
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));
|
||||
}
|
||||
},
|
||||
|
||||
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<TokenStream> {
|
||||
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::<Punctuated<_, Comma>>();
|
||||
|
||||
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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
| ^^^^^^^^^
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -58,20 +58,21 @@ fn comp1<T1, T2>(_props: &()) -> ::yew::Html {
|
||||
}
|
||||
}
|
||||
|
||||
#[::yew::function_component(ConstGenerics)]
|
||||
fn const_generics<const N: ::std::primitive::i32>() -> ::yew::Html {
|
||||
::yew::html! {
|
||||
<div>
|
||||
{ N }
|
||||
</div>
|
||||
}
|
||||
}
|
||||
// no longer possible?
|
||||
// #[::yew::function_component(ConstGenerics)]
|
||||
// fn const_generics<const N: ::std::primitive::i32>() -> ::yew::Html {
|
||||
// ::yew::html! {
|
||||
// <div>
|
||||
// { N }
|
||||
// </div>
|
||||
// }
|
||||
// }
|
||||
|
||||
fn compile_pass() {
|
||||
::yew::html! { <Comp<Props> a=10 /> };
|
||||
::yew::html! { <Comp1<::std::primitive::usize, ::std::primitive::usize> /> };
|
||||
|
||||
::yew::html! { <ConstGenerics<10> /> };
|
||||
// ::yew::html! { <ConstGenerics<10> /> };
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
|
||||
@ -16,27 +16,41 @@ error[E0599]: no method named `build` found for struct `PropsBuilder<PropsBuilde
|
||||
22 | html! { <Comp<Props> /> };
|
||||
| ^^^^ method not found in `PropsBuilder<PropsBuilderStep_missing_required_prop_a>`
|
||||
|
||||
error[E0277]: the trait bound `FunctionComponent<CompFunctionProvider<MissingTypeBounds>>: BaseComponent` is not satisfied
|
||||
--> tests/function_component_attr/generic-props-fail.rs:27:14
|
||||
|
|
||||
27 | html! { <Comp<MissingTypeBounds> /> };
|
||||
| ^^^^ the trait `BaseComponent` is not implemented for `FunctionComponent<CompFunctionProvider<MissingTypeBounds>>`
|
||||
|
|
||||
= help: the following implementations were found:
|
||||
<FunctionComponent<T> as BaseComponent>
|
||||
|
||||
error[E0599]: the function or associated item `new` exists for struct `VChild<FunctionComponent<CompFunctionProvider<MissingTypeBounds>>>`, but its trait bounds were not satisfied
|
||||
--> tests/function_component_attr/generic-props-fail.rs:27:14
|
||||
|
|
||||
27 | html! { <Comp<MissingTypeBounds> /> };
|
||||
| ^^^^ function or associated item cannot be called on `VChild<FunctionComponent<CompFunctionProvider<MissingTypeBounds>>>` due to unsatisfied trait bounds
|
||||
|
|
||||
::: $WORKSPACE/packages/yew/src/functional/mod.rs
|
||||
|
|
||||
| pub struct FunctionComponent<T: FunctionProvider + 'static> {
|
||||
| ----------------------------------------------------------- doesn't satisfy `_: BaseComponent`
|
||||
|
|
||||
= note: the following trait bounds were not satisfied:
|
||||
`FunctionComponent<CompFunctionProvider<MissingTypeBounds>>: BaseComponent`
|
||||
|
||||
error[E0277]: the trait bound `MissingTypeBounds: yew::Properties` is not satisfied
|
||||
--> tests/function_component_attr/generic-props-fail.rs:27:14
|
||||
|
|
||||
27 | html! { <Comp<MissingTypeBounds> /> };
|
||||
| ^^^^ the trait `yew::Properties` is not implemented for `MissingTypeBounds`
|
||||
|
|
||||
= note: required because of the requirements on the impl of `FunctionProvider` for `compFunctionProvider<MissingTypeBounds>`
|
||||
|
||||
error[E0599]: the function or associated item `new` exists for struct `VChild<FunctionComponent<compFunctionProvider<MissingTypeBounds>>>`, but its trait bounds were not satisfied
|
||||
--> tests/function_component_attr/generic-props-fail.rs:27:14
|
||||
|
|
||||
27 | html! { <Comp<MissingTypeBounds> /> };
|
||||
| ^^^^ function or associated item cannot be called on `VChild<FunctionComponent<compFunctionProvider<MissingTypeBounds>>>` due to unsatisfied trait bounds
|
||||
|
|
||||
::: $WORKSPACE/packages/yew/src/functional/mod.rs
|
||||
|
|
||||
| pub struct FunctionComponent<T: FunctionProvider + 'static> {
|
||||
| ----------------------------------------------------------- doesn't satisfy `_: yew::Component`
|
||||
| ---------------- required by this bound in `FunctionComponent`
|
||||
|
|
||||
= note: the following trait bounds were not satisfied:
|
||||
`FunctionComponent<compFunctionProvider<MissingTypeBounds>>: yew::Component`
|
||||
= note: required because of the requirements on the impl of `FunctionProvider` for `CompFunctionProvider<MissingTypeBounds>`
|
||||
|
||||
error[E0107]: missing generics for type alias `Comp`
|
||||
--> tests/function_component_attr/generic-props-fail.rs:30:14
|
||||
|
||||
@ -3,15 +3,17 @@ error[E0277]: the trait bound `Unimplemented: yew::Component` is not satisfied
|
||||
|
|
||||
6 | html! { <Unimplemented /> };
|
||||
| ^^^^^^^^^^^^^ 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<Unimplemented>`, 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! { <Unimplemented /> };
|
||||
| ^^^^^^^^^^^^^ function or associated item cannot be called on `VChild<Unimplemented>` due to unsatisfied trait bounds
|
||||
|
|
||||
= note: the following trait bounds were not satisfied:
|
||||
`Unimplemented: yew::Component`
|
||||
`Unimplemented: BaseComponent`
|
||||
|
||||
@ -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 = []
|
||||
|
||||
@ -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<COMP: Component> {
|
||||
pub struct AppHandle<COMP: BaseComponent> {
|
||||
/// `Scope` holder
|
||||
pub(crate) scope: Scope<COMP>,
|
||||
}
|
||||
|
||||
impl<COMP> AppHandle<COMP>
|
||||
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<COMP> Deref for AppHandle<COMP>
|
||||
where
|
||||
COMP: Component,
|
||||
COMP: BaseComponent,
|
||||
{
|
||||
type Target = Scope<COMP>;
|
||||
|
||||
|
||||
@ -77,7 +77,8 @@ pub fn use_ref<T: 'static>(initial_value: impl FnOnce() -> T) -> Rc<T> {
|
||||
/// ```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};
|
||||
///
|
||||
|
||||
@ -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<T: 'static> Component for FunctionComponent<T>
|
||||
impl<T: 'static> BaseComponent for FunctionComponent<T>
|
||||
where
|
||||
T: FunctionProvider,
|
||||
{
|
||||
@ -137,7 +137,11 @@ where
|
||||
msg()
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> HtmlResult {
|
||||
self.with_hook_state(|| T::run(&*ctx.props()))
|
||||
}
|
||||
|
||||
@ -192,6 +196,7 @@ pub struct HookUpdater {
|
||||
hook: Rc<RefCell<dyn std::any::Any>>,
|
||||
process_message: ProcessMessage,
|
||||
}
|
||||
|
||||
impl HookUpdater {
|
||||
/// Callback which runs the hook.
|
||||
pub fn callback<T: 'static, F>(&self, cb: F)
|
||||
|
||||
@ -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<COMP: Component> {
|
||||
pub(crate) struct ComponentState<COMP: BaseComponent> {
|
||||
pub(crate) component: Box<COMP>,
|
||||
pub(crate) root_node: VNode,
|
||||
|
||||
@ -17,12 +20,14 @@ pub(crate) struct ComponentState<COMP: Component> {
|
||||
node_ref: NodeRef,
|
||||
has_rendered: bool,
|
||||
|
||||
suspension: Option<Suspension>,
|
||||
|
||||
// Used for debug logging
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) vcomp_id: u64,
|
||||
}
|
||||
|
||||
impl<COMP: Component> ComponentState<COMP> {
|
||||
impl<COMP: BaseComponent> ComponentState<COMP> {
|
||||
pub(crate) fn new(
|
||||
parent: Element,
|
||||
next_sibling: NodeRef,
|
||||
@ -47,6 +52,7 @@ impl<COMP: Component> ComponentState<COMP> {
|
||||
parent,
|
||||
next_sibling,
|
||||
node_ref,
|
||||
suspension: None,
|
||||
has_rendered: false,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@ -55,7 +61,7 @@ impl<COMP: Component> ComponentState<COMP> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct CreateRunner<COMP: Component> {
|
||||
pub(crate) struct CreateRunner<COMP: BaseComponent> {
|
||||
pub(crate) parent: Element,
|
||||
pub(crate) next_sibling: NodeRef,
|
||||
pub(crate) placeholder: VNode,
|
||||
@ -64,7 +70,7 @@ pub(crate) struct CreateRunner<COMP: Component> {
|
||||
pub(crate) scope: Scope<COMP>,
|
||||
}
|
||||
|
||||
impl<COMP: Component> Runnable for CreateRunner<COMP> {
|
||||
impl<COMP: BaseComponent> Runnable for CreateRunner<COMP> {
|
||||
fn run(self: Box<Self>) {
|
||||
let mut current_state = self.scope.state.borrow_mut();
|
||||
if current_state.is_none() {
|
||||
@ -83,21 +89,23 @@ impl<COMP: Component> Runnable for CreateRunner<COMP> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum UpdateEvent<COMP: Component> {
|
||||
pub(crate) enum UpdateEvent<COMP: BaseComponent> {
|
||||
/// Wraps messages for a component.
|
||||
Message(COMP::Message),
|
||||
/// Wraps batch of messages for a component.
|
||||
MessageBatch(Vec<COMP::Message>),
|
||||
/// Wraps properties, node ref, and next sibling for a component.
|
||||
Properties(Rc<COMP::Properties>, NodeRef, NodeRef),
|
||||
/// Shift Scope.
|
||||
Shift(Element, NodeRef),
|
||||
}
|
||||
|
||||
pub(crate) struct UpdateRunner<COMP: Component> {
|
||||
pub(crate) struct UpdateRunner<COMP: BaseComponent> {
|
||||
pub(crate) state: Shared<Option<ComponentState<COMP>>>,
|
||||
pub(crate) event: UpdateEvent<COMP>,
|
||||
}
|
||||
|
||||
impl<COMP: Component> Runnable for UpdateRunner<COMP> {
|
||||
impl<COMP: BaseComponent> Runnable for UpdateRunner<COMP> {
|
||||
fn run(self: Box<Self>) {
|
||||
if let Some(mut state) = self.state.borrow_mut().as_mut() {
|
||||
let schedule_render = match self.event {
|
||||
@ -120,6 +128,16 @@ impl<COMP: Component> Runnable for UpdateRunner<COMP> {
|
||||
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<COMP: Component> Runnable for UpdateRunner<COMP> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DestroyRunner<COMP: Component> {
|
||||
pub(crate) struct DestroyRunner<COMP: BaseComponent> {
|
||||
pub(crate) state: Shared<Option<ComponentState<COMP>>>,
|
||||
}
|
||||
|
||||
impl<COMP: Component> Runnable for DestroyRunner<COMP> {
|
||||
impl<COMP: BaseComponent> Runnable for DestroyRunner<COMP> {
|
||||
fn run(self: Box<Self>) {
|
||||
if let Some(mut state) = self.state.borrow_mut().take() {
|
||||
#[cfg(debug_assertions)]
|
||||
@ -161,33 +179,99 @@ impl<COMP: Component> Runnable for DestroyRunner<COMP> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RenderRunner<COMP: Component> {
|
||||
pub(crate) struct RenderRunner<COMP: BaseComponent> {
|
||||
pub(crate) state: Shared<Option<ComponentState<COMP>>>,
|
||||
}
|
||||
|
||||
impl<COMP: Component> Runnable for RenderRunner<COMP> {
|
||||
impl<COMP: BaseComponent> Runnable for RenderRunner<COMP> {
|
||||
fn run(self: Box<Self>) {
|
||||
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::<Suspense>().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::<Suspense>().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<COMP: Component> {
|
||||
pub(crate) struct RenderedRunner<COMP: BaseComponent> {
|
||||
pub(crate) state: Shared<Option<ComponentState<COMP>>>,
|
||||
}
|
||||
|
||||
impl<COMP: Component> Runnable for RenderedRunner<COMP> {
|
||||
impl<COMP: BaseComponent> Runnable for RenderedRunner<COMP> {
|
||||
fn run(self: Box<Self>) {
|
||||
if let Some(state) = self.state.borrow_mut().as_mut() {
|
||||
#[cfg(debug_assertions)]
|
||||
|
||||
@ -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<COMP: Component> {
|
||||
pub struct Context<COMP: BaseComponent> {
|
||||
pub(crate) scope: Scope<COMP>,
|
||||
pub(crate) props: Rc<COMP::Properties>,
|
||||
}
|
||||
|
||||
impl<COMP: Component> Context<COMP> {
|
||||
impl<COMP: BaseComponent> Context<COMP> {
|
||||
/// The component scope
|
||||
#[inline]
|
||||
pub fn link(&self) -> &Scope<COMP> {
|
||||
@ -34,6 +34,38 @@ impl<COMP: Component> Context<COMP> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>) -> Self;
|
||||
|
||||
/// Updates component's internal state.
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool;
|
||||
|
||||
/// React to changes of component properties.
|
||||
fn changed(&mut self, ctx: &Context<Self>) -> bool;
|
||||
|
||||
/// Returns a component layout to be rendered.
|
||||
fn view(&self, ctx: &Context<Self>) -> HtmlResult;
|
||||
|
||||
/// Notified after a layout is rendered.
|
||||
fn rendered(&mut self, ctx: &Context<Self>, first_render: bool);
|
||||
|
||||
/// Notified before a component is destroyed.
|
||||
fn destroy(&mut self, ctx: &Context<Self>);
|
||||
}
|
||||
|
||||
/// 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<Self>) {}
|
||||
}
|
||||
|
||||
impl<T> BaseComponent for T
|
||||
where
|
||||
T: Sized + Component + 'static,
|
||||
{
|
||||
type Message = <T as Component>::Message;
|
||||
|
||||
type Properties = <T as Component>::Properties;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
Component::create(ctx)
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
Component::update(self, ctx, msg)
|
||||
}
|
||||
|
||||
fn changed(&mut self, ctx: &Context<Self>) -> bool {
|
||||
Component::changed(self, ctx)
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> HtmlResult {
|
||||
Component::view(self, ctx).into_html_result()
|
||||
}
|
||||
|
||||
fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
|
||||
Component::rendered(self, ctx, first_render)
|
||||
}
|
||||
|
||||
fn destroy(&mut self, ctx: &Context<Self>) {
|
||||
Component::destroy(self, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<COMP: Component> From<Scope<COMP>> for AnyScope {
|
||||
impl<COMP: BaseComponent> From<Scope<COMP>> for AnyScope {
|
||||
fn from(scope: Scope<COMP>) -> Self {
|
||||
AnyScope {
|
||||
type_id: TypeId::of::<COMP>(),
|
||||
@ -71,7 +71,7 @@ impl AnyScope {
|
||||
}
|
||||
|
||||
/// Attempts to downcast into a typed scope
|
||||
pub fn downcast<COMP: Component>(self) -> Scope<COMP> {
|
||||
pub fn downcast<COMP: BaseComponent>(self) -> Scope<COMP> {
|
||||
let state = self
|
||||
.state
|
||||
.downcast::<RefCell<Option<ComponentState<COMP>>>>()
|
||||
@ -93,7 +93,7 @@ impl AnyScope {
|
||||
}
|
||||
}
|
||||
|
||||
fn find_parent_scope<C: Component>(&self) -> Option<Scope<C>> {
|
||||
pub(crate) fn find_parent_scope<C: BaseComponent>(&self) -> Option<Scope<C>> {
|
||||
let expected_type_id = TypeId::of::<C>();
|
||||
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<Ref<'_, VNode>>;
|
||||
fn destroy(&mut self);
|
||||
fn shift_node(&self, parent: Element, next_sibling: NodeRef);
|
||||
}
|
||||
|
||||
impl<COMP: Component> Scoped for Scope<COMP> {
|
||||
impl<COMP: BaseComponent> Scoped for Scope<COMP> {
|
||||
fn to_any(&self) -> AnyScope {
|
||||
self.clone().into()
|
||||
}
|
||||
@ -145,10 +146,17 @@ impl<COMP: Component> Scoped for Scope<COMP> {
|
||||
// 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<COMP: Component> {
|
||||
pub struct Scope<COMP: BaseComponent> {
|
||||
parent: Option<Rc<AnyScope>>,
|
||||
pub(crate) state: Shared<Option<ComponentState<COMP>>>,
|
||||
|
||||
@ -157,13 +165,13 @@ pub struct Scope<COMP: Component> {
|
||||
pub(crate) vcomp_id: u64,
|
||||
}
|
||||
|
||||
impl<COMP: Component> fmt::Debug for Scope<COMP> {
|
||||
impl<COMP: BaseComponent> fmt::Debug for Scope<COMP> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("Scope<_>")
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP: Component> Clone for Scope<COMP> {
|
||||
impl<COMP: BaseComponent> Clone for Scope<COMP> {
|
||||
fn clone(&self) -> Self {
|
||||
Scope {
|
||||
parent: self.parent.clone(),
|
||||
@ -175,7 +183,7 @@ impl<COMP: Component> Clone for Scope<COMP> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP: Component> Scope<COMP> {
|
||||
impl<COMP: BaseComponent> Scope<COMP> {
|
||||
/// Returns the parent scope
|
||||
pub fn get_parent(&self) -> Option<&AnyScope> {
|
||||
self.parent.as_deref()
|
||||
@ -268,7 +276,7 @@ impl<COMP: Component> Scope<COMP> {
|
||||
/// 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<T>(&self, msg: T)
|
||||
where
|
||||
T: Into<COMP::Message>,
|
||||
@ -283,7 +291,7 @@ impl<COMP: Component> Scope<COMP> {
|
||||
/// 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<COMP::Message>) {
|
||||
// There is no reason to schedule empty batches.
|
||||
// This check is especially handy for the batch_callback method.
|
||||
@ -340,7 +348,6 @@ impl<COMP: Component> Scope<COMP> {
|
||||
};
|
||||
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<COMP: Component> Scope<COMP> {
|
||||
|
||||
/// 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<COMP: Component> {
|
||||
pub trait SendAsMessage<COMP: BaseComponent> {
|
||||
/// Sends the message to the given component's scope.
|
||||
/// See [Scope::batch_callback](struct.Scope.html#method.batch_callback).
|
||||
fn send(self, scope: &Scope<COMP>);
|
||||
@ -417,7 +424,7 @@ pub trait SendAsMessage<COMP: Component> {
|
||||
|
||||
impl<COMP> SendAsMessage<COMP> for Option<COMP::Message>
|
||||
where
|
||||
COMP: Component,
|
||||
COMP: BaseComponent,
|
||||
{
|
||||
fn send(self, scope: &Scope<COMP>) {
|
||||
if let Some(msg) = self {
|
||||
@ -428,7 +435,7 @@ where
|
||||
|
||||
impl<COMP> SendAsMessage<COMP> for Vec<COMP::Message>
|
||||
where
|
||||
COMP: Component,
|
||||
COMP: BaseComponent,
|
||||
{
|
||||
fn send(self, scope: &Scope<COMP>) {
|
||||
scope.send_message_batch(self);
|
||||
|
||||
@ -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
|
||||
|
||||
14
packages/yew/src/html/error.rs
Normal file
14
packages/yew/src/html/error.rs
Normal file
@ -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<T> = std::result::Result<T, RenderError>;
|
||||
@ -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<Html>;
|
||||
|
||||
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
|
||||
|
||||
@ -160,7 +160,6 @@ pub use yew_macro::html;
|
||||
/// impl Into<Html> for ListItem {
|
||||
/// fn into(self) -> Html { html! { <self /> } }
|
||||
/// }
|
||||
///
|
||||
/// // 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<bool> = 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<COMP>(element: Element) -> AppHandle<COMP>
|
||||
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<COMP>() -> AppHandle<COMP>
|
||||
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<COMP>() -> AppHandle<COMP>
|
||||
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<COMP>(
|
||||
props: COMP::Properties,
|
||||
) -> AppHandle<COMP>
|
||||
where
|
||||
COMP: Component,
|
||||
COMP: BaseComponent,
|
||||
{
|
||||
set_default_panic_hook();
|
||||
AppHandle::<COMP>::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<COMP>(props: COMP::Properties) -> AppHandle<COMP>
|
||||
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<COMP>(props: COMP::Properties) -> AppHandle<COMP>
|
||||
where
|
||||
COMP: Component,
|
||||
COMP: BaseComponent,
|
||||
{
|
||||
set_default_panic_hook();
|
||||
AppHandle::<COMP>::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::*;
|
||||
}
|
||||
|
||||
2
packages/yew/src/sealed.rs
Normal file
2
packages/yew/src/sealed.rs
Normal file
@ -0,0 +1,2 @@
|
||||
/// Base traits for sealed traits.
|
||||
pub trait Sealed {}
|
||||
99
packages/yew/src/suspense/component.rs
Normal file
99
packages/yew/src/suspense/component.rs
Normal file
@ -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<Key>,
|
||||
}
|
||||
|
||||
#[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<Self>,
|
||||
suspensions: Vec<Suspension>,
|
||||
detached_parent: Element,
|
||||
}
|
||||
|
||||
impl Component for Suspense {
|
||||
type Properties = SuspenseProps;
|
||||
type Message = SuspenseMsg;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
Self {
|
||||
link: ctx.link().clone(),
|
||||
suspensions: Vec::new(),
|
||||
detached_parent: document().create_element("div").unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, 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<Self>) -> 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));
|
||||
}
|
||||
}
|
||||
7
packages/yew/src/suspense/mod.rs
Normal file
7
packages/yew/src/suspense/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
//! This module provides suspense support.
|
||||
|
||||
mod component;
|
||||
mod suspension;
|
||||
|
||||
pub use component::Suspense;
|
||||
pub use suspension::{Suspension, SuspensionHandle, SuspensionResult};
|
||||
140
packages/yew/src/suspense/suspension.rs
Normal file
140
packages/yew/src/suspense/suspension.rs
Normal file
@ -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<usize> = 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<RefCell<Vec<Callback<Self>>>>,
|
||||
|
||||
resumed: Rc<AtomicBool>,
|
||||
}
|
||||
|
||||
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<Output = ()> + '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<Self>) {
|
||||
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<Self::Output> {
|
||||
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<T> = std::result::Result<T, Suspension>;
|
||||
|
||||
/// 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();
|
||||
}
|
||||
}
|
||||
@ -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<AttrValue>]>,
|
||||
},
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<COMP: Component> {
|
||||
pub struct VChild<COMP: BaseComponent> {
|
||||
/// The component properties
|
||||
pub props: Rc<COMP::Properties>,
|
||||
/// Reference to the mounted node
|
||||
@ -81,7 +81,7 @@ pub struct VChild<COMP: Component> {
|
||||
key: Option<Key>,
|
||||
}
|
||||
|
||||
impl<COMP: Component> Clone for VChild<COMP> {
|
||||
impl<COMP: BaseComponent> Clone for VChild<COMP> {
|
||||
fn clone(&self) -> Self {
|
||||
VChild {
|
||||
props: Rc::clone(&self.props),
|
||||
@ -91,7 +91,7 @@ impl<COMP: Component> Clone for VChild<COMP> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP: Component> PartialEq for VChild<COMP>
|
||||
impl<COMP: BaseComponent> PartialEq for VChild<COMP>
|
||||
where
|
||||
COMP::Properties: PartialEq,
|
||||
{
|
||||
@ -102,7 +102,7 @@ where
|
||||
|
||||
impl<COMP> VChild<COMP>
|
||||
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<Key>) -> Self {
|
||||
@ -116,7 +116,7 @@ where
|
||||
|
||||
impl<COMP> From<VChild<COMP>> for VComp
|
||||
where
|
||||
COMP: Component,
|
||||
COMP: BaseComponent,
|
||||
{
|
||||
fn from(vchild: VChild<COMP>) -> Self {
|
||||
VComp::new::<COMP>(vchild.props, vchild.node_ref, vchild.key)
|
||||
@ -127,7 +127,7 @@ impl VComp {
|
||||
/// Creates a new `VComp` instance.
|
||||
pub fn new<COMP>(props: Rc<COMP::Properties>, node_ref: NodeRef, key: Option<Key>) -> Self
|
||||
where
|
||||
COMP: Component,
|
||||
COMP: BaseComponent,
|
||||
{
|
||||
VComp {
|
||||
type_id: TypeId::of::<COMP>(),
|
||||
@ -183,17 +183,17 @@ trait Mountable {
|
||||
fn reuse(self: Box<Self>, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef);
|
||||
}
|
||||
|
||||
struct PropsWrapper<COMP: Component> {
|
||||
struct PropsWrapper<COMP: BaseComponent> {
|
||||
props: Rc<COMP::Properties>,
|
||||
}
|
||||
|
||||
impl<COMP: Component> PropsWrapper<COMP> {
|
||||
impl<COMP: BaseComponent> PropsWrapper<COMP> {
|
||||
pub fn new(props: Rc<COMP::Properties>) -> Self {
|
||||
Self { props }
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP: Component> Mountable for PropsWrapper<COMP> {
|
||||
impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
|
||||
fn copy(&self) -> Box<dyn Mountable> {
|
||||
let wrapper: PropsWrapper<COMP> = 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<COMP: Component> fmt::Debug for VChild<COMP> {
|
||||
impl<COMP: BaseComponent> fmt::Debug for VChild<COMP> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("VChild<_>")
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<VComp> for VNode {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VSuspense> for VNode {
|
||||
#[inline]
|
||||
fn from(vsuspense: VSuspense) -> Self {
|
||||
VNode::VSuspense(vsuspense)
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP> From<VChild<COMP>> for VNode
|
||||
where
|
||||
COMP: Component,
|
||||
COMP: BaseComponent,
|
||||
{
|
||||
fn from(vchild: VChild<COMP>) -> 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
147
packages/yew/src/virtual_dom/vsuspense.rs
Normal file
147
packages/yew/src/virtual_dom/vsuspense.rs
Normal file
@ -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<VNode>,
|
||||
|
||||
/// Fallback nodes when suspended.
|
||||
fallback: Box<VNode>,
|
||||
|
||||
/// 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<Key>,
|
||||
}
|
||||
|
||||
impl VSuspense {
|
||||
pub(crate) fn new(
|
||||
children: VNode,
|
||||
fallback: VNode,
|
||||
detached_parent: Element,
|
||||
suspended: bool,
|
||||
key: Option<Key>,
|
||||
) -> Self {
|
||||
Self {
|
||||
children: children.into(),
|
||||
fallback: fallback.into(),
|
||||
detached_parent,
|
||||
suspended,
|
||||
key,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn first_node(&self) -> Option<Node> {
|
||||
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<VNode>,
|
||||
) -> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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! {
|
||||
<div id="result">
|
||||
{"done"}
|
||||
</div>
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
type PropsComponent = FunctionComponent<PropsPassedFunction>;
|
||||
|
||||
401
packages/yew/tests/suspense.rs
Normal file
401
packages/yew/tests/suspense.rs
Normal file
@ -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<Self>, _action: Self::Action) -> Rc<Self> {
|
||||
Self::new().into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
||||
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! {
|
||||
<div class="content-area">
|
||||
<div class="actual-result">{*value}</div>
|
||||
<button class="increase" onclick={on_increment}>{"increase"}</button>
|
||||
<div class="action-area">
|
||||
<button class="take-a-break" onclick={on_take_a_break}>{"Take a break!"}</button>
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
|
||||
#[function_component(App)]
|
||||
fn app() -> Html {
|
||||
let fallback = html! {<div>{"wait..."}</div>};
|
||||
|
||||
html! {
|
||||
<div id="result">
|
||||
<Suspense {fallback}>
|
||||
<Content />
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
yew::start_app_in_element::<App>(gloo_utils::document().get_element_by_id("output").unwrap());
|
||||
|
||||
TimeoutFuture::new(10).await;
|
||||
let result = obtain_result();
|
||||
assert_eq!(result.as_str(), "<div>wait...</div>");
|
||||
|
||||
TimeoutFuture::new(50).await;
|
||||
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><div class="actual-result">0</div><button class="increase">increase</button><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
|
||||
);
|
||||
|
||||
TimeoutFuture::new(10).await;
|
||||
|
||||
gloo_utils::document()
|
||||
.query_selector(".increase")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
.click();
|
||||
|
||||
gloo_utils::document()
|
||||
.query_selector(".increase")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
.click();
|
||||
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><div class="actual-result">2</div><button class="increase">increase</button><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
|
||||
);
|
||||
|
||||
gloo_utils::document()
|
||||
.query_selector(".take-a-break")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
.click();
|
||||
|
||||
TimeoutFuture::new(10).await;
|
||||
let result = obtain_result();
|
||||
assert_eq!(result.as_str(), "<div>wait...</div>");
|
||||
|
||||
TimeoutFuture::new(50).await;
|
||||
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><div class="actual-result">2</div><button class="increase">increase</button><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn suspense_not_suspended_at_start() {
|
||||
#[derive(PartialEq)]
|
||||
pub struct SleepState {
|
||||
s: Option<Suspension>,
|
||||
}
|
||||
|
||||
impl SleepState {
|
||||
fn new() -> Self {
|
||||
Self { s: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl Reducible for SleepState {
|
||||
type Action = ();
|
||||
|
||||
fn reduce(self: Rc<Self>, _action: Self::Action) -> Rc<Self> {
|
||||
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<Rc<dyn Fn()>> {
|
||||
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! {
|
||||
<div class="content-area">
|
||||
<textarea value={value.to_string()} oninput={on_text_input}></textarea>
|
||||
<div class="action-area">
|
||||
<button class="take-a-break" onclick={on_take_a_break}>{"Take a break!"}</button>
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
|
||||
#[function_component(App)]
|
||||
fn app() -> Html {
|
||||
let fallback = html! {<div>{"wait..."}</div>};
|
||||
|
||||
html! {
|
||||
<div id="result">
|
||||
<Suspense {fallback}>
|
||||
<Content />
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
yew::start_app_in_element::<App>(gloo_utils::document().get_element_by_id("output").unwrap());
|
||||
|
||||
TimeoutFuture::new(10).await;
|
||||
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><textarea></textarea><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
|
||||
);
|
||||
gloo_utils::document()
|
||||
.query_selector(".take-a-break")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
.click();
|
||||
|
||||
TimeoutFuture::new(10).await;
|
||||
let result = obtain_result();
|
||||
assert_eq!(result.as_str(), "<div>wait...</div>");
|
||||
|
||||
TimeoutFuture::new(50).await;
|
||||
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><textarea></textarea><div class="action-area"><button class="take-a-break">Take a break!</button></div></div>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[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<Self>, _action: Self::Action) -> Rc<Self> {
|
||||
Self::new().into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn use_sleep() -> SuspensionResult<Rc<dyn Fn()>> {
|
||||
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! {
|
||||
<div class="content-area">
|
||||
<div class="action-area">
|
||||
<button class="take-a-break2" onclick={on_take_a_break}>{"Take a break!"}</button>
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
|
||||
#[function_component(Content)]
|
||||
fn content() -> HtmlResult {
|
||||
let resleep = use_sleep()?;
|
||||
|
||||
let fallback = html! {<div>{"wait...(inner)"}</div>};
|
||||
|
||||
let on_take_a_break = Callback::from(move |_: MouseEvent| (resleep.clone())());
|
||||
|
||||
Ok(html! {
|
||||
<div class="content-area">
|
||||
<div class="action-area">
|
||||
<button class="take-a-break" onclick={on_take_a_break}>{"Take a break!"}</button>
|
||||
</div>
|
||||
<Suspense {fallback}>
|
||||
<InnerContent />
|
||||
</Suspense>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
|
||||
#[function_component(App)]
|
||||
fn app() -> Html {
|
||||
let fallback = html! {<div>{"wait...(outer)"}</div>};
|
||||
|
||||
html! {
|
||||
<div id="result">
|
||||
<Suspense {fallback}>
|
||||
<Content />
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
yew::start_app_in_element::<App>(gloo_utils::document().get_element_by_id("output").unwrap());
|
||||
|
||||
TimeoutFuture::new(10).await;
|
||||
let result = obtain_result();
|
||||
assert_eq!(result.as_str(), "<div>wait...(outer)</div>");
|
||||
|
||||
TimeoutFuture::new(50).await;
|
||||
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><div>wait...(inner)</div></div>"#
|
||||
);
|
||||
|
||||
TimeoutFuture::new(50).await;
|
||||
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><div class="content-area"><div class="action-area"><button class="take-a-break2">Take a break!</button></div></div></div>"#
|
||||
);
|
||||
|
||||
gloo_utils::document()
|
||||
.query_selector(".take-a-break2")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
.click();
|
||||
|
||||
TimeoutFuture::new(10).await;
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><div>wait...(inner)</div></div>"#
|
||||
);
|
||||
|
||||
TimeoutFuture::new(50).await;
|
||||
|
||||
let result = obtain_result();
|
||||
assert_eq!(
|
||||
result.as_str(),
|
||||
r#"<div class="content-area"><div class="action-area"><button class="take-a-break">Take a break!</button></div><div class="content-area"><div class="action-area"><button class="take-a-break2">Take a break!</button></div></div></div>"#
|
||||
);
|
||||
}
|
||||
@ -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::<ExampleContext>().is_some() {
|
||||
console_log!(
|
||||
"Context should be None here, but was {:?}!",
|
||||
use_context::<ExampleContext>().unwrap()
|
||||
);
|
||||
};
|
||||
return html! {
|
||||
Ok(html! {
|
||||
<div></div>
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
impl FunctionProvider for UseContextFunctionOuter {
|
||||
type TProps = ();
|
||||
|
||||
fn run(_props: &Self::TProps) -> Html {
|
||||
fn run(_props: &Self::TProps) -> HtmlResult {
|
||||
type ExampleContextProvider = ContextProvider<ExampleContext>;
|
||||
return html! {
|
||||
Ok(html! {
|
||||
<div>
|
||||
<ExampleContextProvider context={ExampleContext("wrong1".into())}>
|
||||
<div>{"ignored"}</div>
|
||||
@ -58,17 +58,17 @@ fn use_context_scoping_works() {
|
||||
</ExampleContextProvider>
|
||||
<ExpectNoContextComponent />
|
||||
</div>
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
impl FunctionProvider for UseContextFunctionInner {
|
||||
type TProps = ();
|
||||
|
||||
fn run(_props: &Self::TProps) -> Html {
|
||||
fn run(_props: &Self::TProps) -> HtmlResult {
|
||||
let context = use_context::<ExampleContext>();
|
||||
return html! {
|
||||
Ok(html! {
|
||||
<div id="result">{ &context.unwrap().0 }</div>
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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::<ContextA>(), Some(ContextA(2)));
|
||||
assert_eq!(use_context::<ContextB>(), Some(ContextB(1)));
|
||||
|
||||
return html! {};
|
||||
Ok(html! {})
|
||||
}
|
||||
}
|
||||
type Test1 = FunctionComponent<Test1Function>;
|
||||
@ -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::<ContextA>(), Some(ContextA(0)));
|
||||
assert_eq!(use_context::<ContextB>(), Some(ContextB(1)));
|
||||
|
||||
return html! {};
|
||||
Ok(html! {})
|
||||
}
|
||||
}
|
||||
type Test2 = FunctionComponent<Test2Function>;
|
||||
@ -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::<ContextA>(), Some(ContextA(0)));
|
||||
assert_eq!(use_context::<ContextB>(), None);
|
||||
|
||||
return html! {};
|
||||
Ok(html! {})
|
||||
}
|
||||
}
|
||||
type Test3 = FunctionComponent<Test3Function>;
|
||||
@ -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::<ContextA>(), None);
|
||||
assert_eq!(use_context::<ContextB>(), None);
|
||||
|
||||
return html! {};
|
||||
Ok(html! {})
|
||||
}
|
||||
}
|
||||
type Test4 = FunctionComponent<Test4Function>;
|
||||
@ -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<ContextA>;
|
||||
type ContextBProvider = ContextProvider<ContextB>;
|
||||
|
||||
return html! {
|
||||
Ok(html! {
|
||||
<div>
|
||||
<ContextAProvider context={ContextA(0)}>
|
||||
<ContextBProvider context={ContextB(1)}>
|
||||
@ -159,7 +159,7 @@ fn use_context_works_with_multiple_types() {
|
||||
</ContextAProvider>
|
||||
<Test4 />
|
||||
</div>
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
type TestComponent = FunctionComponent<TestFunction>;
|
||||
@ -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! {
|
||||
<>
|
||||
<div id={props.id.clone()}>
|
||||
{ format!("total: {}", counter.borrow()) }
|
||||
</div>
|
||||
{ props.children.clone() }
|
||||
</>
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
type RenderCounter = FunctionComponent<RenderCounterFunction>;
|
||||
@ -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::<Rc<MyContext>>().expect("context not passed down");
|
||||
|
||||
return html! {
|
||||
Ok(html! {
|
||||
<>
|
||||
<div>{ format!("magic: {}\n", props.magic) }</div>
|
||||
<div id={props.id.clone()}>
|
||||
{ format!("current: {}, total: {}", ctx.0, counter.borrow()) }
|
||||
</div>
|
||||
</>
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
type ContextOutlet = FunctionComponent<ContextOutletFunction>;
|
||||
@ -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<Rc<MyContext>>;
|
||||
|
||||
let ctx = use_state(|| MyContext("hello".into()));
|
||||
@ -263,14 +263,14 @@ fn use_context_update_works() {
|
||||
|| {}
|
||||
});
|
||||
}
|
||||
return html! {
|
||||
Ok(html! {
|
||||
<MyContextProvider context={Rc::new((*ctx).clone())}>
|
||||
<RenderCounter id="test-0">
|
||||
<ContextOutlet id="test-1"/>
|
||||
<ContextOutlet id="test-2" {magic}/>
|
||||
</RenderCounter>
|
||||
</MyContextProvider>
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
type TestComponent = FunctionComponent<TestFunction>;
|
||||
|
||||
@ -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<dyn Fn()> = { Rc::new(move || show.set(false)) };
|
||||
html! {
|
||||
Ok(html! {
|
||||
<UseEffectComponent destroy_called={props.destroy_called.clone()} {effect_called} />
|
||||
}
|
||||
})
|
||||
} else {
|
||||
html! {
|
||||
Ok(html! {
|
||||
<div>{ "EMPTY" }</div>
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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! {
|
||||
<div>
|
||||
{ "The test result is" }
|
||||
<div id="result">{ *counter }</div>
|
||||
{ "\n" }
|
||||
</div>
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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! {
|
||||
<div>
|
||||
{ "The test result is" }
|
||||
<div id="result">{ *counter }</div>
|
||||
{ "\n" }
|
||||
</div>
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
type UseEffectComponent = FunctionComponent<UseEffectFunction>;
|
||||
@ -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! {
|
||||
<div>
|
||||
{"The test result is"}
|
||||
<div id="result">{*number_ref.borrow_mut().deref_mut()}{*number_ref2.borrow_mut().deref_mut()}</div>
|
||||
{"\n"}
|
||||
</div>
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
type UseEffectComponent = FunctionComponent<UseEffectFunction>;
|
||||
|
||||
@ -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! {
|
||||
<div>
|
||||
{"The test result is"}
|
||||
<div id="result">{counter.counter}</div>
|
||||
{"\n"}
|
||||
</div>
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
type UseReducerComponent = FunctionComponent<UseReducerFunction>;
|
||||
@ -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! {
|
||||
<>
|
||||
<div>
|
||||
{"This component has been rendered: "}<span id="result">{render_count}</span>{" Time(s)."}
|
||||
@ -112,7 +112,7 @@ fn use_reducer_eq_works() {
|
||||
<button onclick={add_content_a} id="add-a">{"Add A to Content"}</button>
|
||||
<button onclick={add_content_b} id="add-b">{"Add B to Content"}</button>
|
||||
</>
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
type UseReducerComponent = FunctionComponent<UseReducerFunction>;
|
||||
|
||||
@ -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! {
|
||||
<div>
|
||||
{"The test output is: "}
|
||||
<div id="result">{*ref_example.borrow_mut().deref_mut() > 4}</div>
|
||||
{"\n"}
|
||||
</div>
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
type UseRefComponent = FunctionComponent<UseRefFunction>;
|
||||
|
||||
@ -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! {
|
||||
<div>
|
||||
{"Test Output: "}
|
||||
<div id="result">{*counter}</div>
|
||||
{"\n"}
|
||||
</div>
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
type UseComponent = FunctionComponent<UseStateFunction>;
|
||||
@ -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! {
|
||||
<div>
|
||||
{ "Test Output: " }
|
||||
// expected output
|
||||
<div id="result">{ *counter }</div>
|
||||
{ "\n" }
|
||||
</div>
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
type UseComponent = FunctionComponent<UseStateFunction>;
|
||||
@ -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! {
|
||||
<div>
|
||||
{"Test Output: "}
|
||||
<div id="result">{*counter}</div>
|
||||
{"\n"}
|
||||
</div>
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
type UseComponent = FunctionComponent<UseStateFunction>;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -54,7 +54,7 @@ html! {
|
||||
<TabItem value="With props" label="With props">
|
||||
|
||||
```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 {
|
||||
<TabItem value="Without props" label="Without props">
|
||||
|
||||
```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<T>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
180
website/docs/concepts/suspense.md
Normal file
180
website/docs/concepts/suspense.md
Normal file
@ -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! {<div>{"Hello, "}{&user.name}</div>})
|
||||
}
|
||||
|
||||
#[function_component(App)]
|
||||
fn app() -> Html {
|
||||
let fallback = html! {<div>{"Loading..."}</div>};
|
||||
|
||||
html! {
|
||||
<Suspense {fallback}>
|
||||
<Content />
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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<T>`. 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<User> {
|
||||
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<User> {
|
||||
todo!() // implementation omitted.
|
||||
}
|
||||
|
||||
fn on_load_user_complete<F: Fn()>(_fn: F) {
|
||||
todo!() // implementation omitted.
|
||||
}
|
||||
|
||||
fn use_user() -> SuspensionResult<User> {
|
||||
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! {<div>{"Hello, "}{&user.name}</div>})
|
||||
}
|
||||
|
||||
#[function_component(App)]
|
||||
fn app() -> Html {
|
||||
let fallback = html! {<div>{"Loading..."}</div>};
|
||||
|
||||
html! {
|
||||
<Suspense {fallback}>
|
||||
<Content />
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 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<T>() -> HtmlResult
|
||||
where T: BaseComponent
|
||||
{
|
||||
let user = use_user()?;
|
||||
|
||||
Ok(html! {<T {user} />})
|
||||
}
|
||||
|
||||
#[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 {
|
||||
Self
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let name = ctx.props().user.name;
|
||||
|
||||
html! {<div>{"Hello, "}{name}{"!"}</div>}
|
||||
}
|
||||
}
|
||||
|
||||
pub type UserContent = WithUser<BaseUserContent>;
|
||||
```
|
||||
|
||||
## Relevant examples
|
||||
|
||||
- [Suspense](https://github.com/yewstack/yew/tree/master/examples/suspense)
|
||||
@ -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: "wasm-bindgen",
|
||||
items: [
|
||||
"concepts/wasm-bindgen/introduction",
|
||||
"concepts/wasm-bindgen/web-sys",
|
||||
],
|
||||
},
|
||||
{
|
||||
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: "Components",
|
||||
items: [
|
||||
"concepts/components/introduction",
|
||||
"concepts/components/callbacks",
|
||||
"concepts/components/scope",
|
||||
"concepts/components/properties",
|
||||
"concepts/components/children",
|
||||
"concepts/components/refs",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Advanced topics',
|
||||
items: [
|
||||
"advanced-topics/how-it-works",
|
||||
"advanced-topics/optimizations",
|
||||
"advanced-topics/portals",
|
||||
]
|
||||
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: 'More',
|
||||
items: [
|
||||
"more/debugging",
|
||||
"more/development-tips",
|
||||
"more/external-libs",
|
||||
"more/css",
|
||||
"more/testing",
|
||||
"more/roadmap",
|
||||
"more/wasm-build-tools"
|
||||
]
|
||||
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",
|
||||
"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: "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: "yew-agent",
|
||||
items: ["migration-guides/yew-agent/from-0_0_0-to-0_1_0"],
|
||||
},
|
||||
"tutorial"
|
||||
],
|
||||
{
|
||||
type: "category",
|
||||
label: "yew-router",
|
||||
items: ["migration-guides/yew-router/from-0_15_0-to-0_16_0"],
|
||||
},
|
||||
],
|
||||
},
|
||||
"tutorial",
|
||||
],
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user