//! Common Component module. //! This is used to factor out some common functionality that is recurrent in modules all over the //! application. In particular: //! - error handling //! - task handling //! - storing props //! //! The pattern used is the //! [CRTP](https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern) pattern: The //! [`CommonComponent`] trait must be implemented with `Self` as the parameter, e.g. //! //! ```ignore //! struct MyComponent; //! impl CommonComponent for MyComponent { ... } //! ``` //! //! The component should also have a `CommonComponentParts` as a field, usually named //! `common`. //! //! Then the [`yew::prelude::Component::update`] method can delegate to //! [`CommonComponentParts::update`]. This will in turn call [`CommonComponent::handle_msg`] and //! take care of error and task handling. use std::{ future::Future, marker::PhantomData, sync::{Arc, Mutex}, }; use crate::infra::api::HostService; use anyhow::{Error, Result}; use gloo_console::error; use graphql_client::GraphQLQuery; use yew::prelude::*; /// Trait required for common components. pub trait CommonComponent>: Component { /// Handle the incoming message. If an error is returned here, any running task will be /// cancelled, the error will be written to the [`CommonComponentParts::error`] and the /// component will be refreshed. fn handle_msg( &mut self, ctx: &Context, msg: ::Message, ) -> Result; /// Get a mutable reference to the inner component parts, necessary for the CRTP. fn mut_common(&mut self) -> &mut CommonComponentParts; } /// Structure that contains the common parts needed by most components. /// The fields of [`props`] are directly accessible through a `Deref` implementation. pub struct CommonComponentParts> { pub error: Option, is_task_running: Arc>, _phantom: PhantomData, } impl> CommonComponentParts { pub fn create() -> Self { CommonComponentParts { error: None, is_task_running: Arc::new(Mutex::new(false)), _phantom: PhantomData::, } } /// Whether there is a currently running task in the background. pub fn is_task_running(&self) -> bool { *self.is_task_running.lock().unwrap() } /// This should be called from the [`yew::prelude::Component::update`]: it will in turn call /// [`CommonComponent::handle_msg`] and handle any resulting error. pub fn update(com: &mut C, ctx: &Context, msg: ::Message) -> bool { com.mut_common().error = None; match com.handle_msg(ctx, msg) { Err(e) => { error!(&e.to_string()); com.mut_common().error = Some(e); assert!(!*com.mut_common().is_task_running.lock().unwrap()); true } Ok(b) => b, } } /// Same as above, but the resulting error is instead passed to the reporting function. pub fn update_and_report_error( com: &mut C, ctx: &Context, msg: ::Message, report_fn: Callback, ) -> bool { let should_render = Self::update(com, ctx, msg); com.mut_common() .error .take() .map(|e| { report_fn.emit(e); true }) .unwrap_or(should_render) } /// Call `method` from the backend with the given `request`, and pass the `callback` for the /// result. pub fn call_backend(&mut self, ctx: &Context, fut: Fut, callback: Cb) where Fut: Future + 'static, Cb: FnOnce(Resp) -> ::Message + 'static, { { let mut running = self.is_task_running.lock().unwrap(); assert!(!*running); *running = true; } let is_task_running = self.is_task_running.clone(); ctx.link().send_future(async move { let res = fut.await; *is_task_running.lock().unwrap() = false; callback(res) }); } /// Call the backend with a GraphQL query. /// /// `EnumCallback` should usually be left as `_`. pub fn call_graphql( &mut self, ctx: &Context, variables: QueryType::Variables, enum_callback: EnumCallback, error_message: &'static str, ) where QueryType: GraphQLQuery + 'static, EnumCallback: Fn(Result) -> ::Message + 'static, { self.call_backend( ctx, HostService::graphql_query::(variables, error_message), enum_callback, ); } }