diff --git a/Cargo.toml b/Cargo.toml index 227bc064108..b6191b5c9ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ members = [ "examples/counter", "examples/crm", "examples/dashboard", + "examples/dyn_create_destroy_apps", "examples/file_upload", "examples/futures", "examples/game_of_life", diff --git a/examples/README.md b/examples/README.md index f194c7dfd58..910b651c0ec 100644 --- a/examples/README.md +++ b/examples/README.md @@ -26,29 +26,30 @@ As an example, check out the TodoMVC example here: "] +edition = "2018" +license = "MIT OR Apache-2.0" + +[dependencies] +js-sys = "0.3" +yew = { path = "../../packages/yew" } +yew-services = { path = "../../packages/yew-services" } +slab = "0.4.3" + + +[dependencies.web-sys] +version = "0.3.50" +features = [ + "Document", + "Element", + "Node", + "DomTokenList" +] diff --git a/examples/dyn_create_destroy_apps/README.md b/examples/dyn_create_destroy_apps/README.md new file mode 100644 index 00000000000..4f43e576587 --- /dev/null +++ b/examples/dyn_create_destroy_apps/README.md @@ -0,0 +1,21 @@ +# Dynamic app creation and destruction example + +An example of how to create and destroy Yew apps on demand. + +## Running + +Run a debug version of this application: + +```bash +trunk serve +``` + +Run a release version of this application: + +```bash +trunk serve --release +``` + +## Concepts + +Demonstrates the use of the Yew app handle by dynamically creating and destroying apps. diff --git a/examples/dyn_create_destroy_apps/index.html b/examples/dyn_create_destroy_apps/index.html new file mode 100644 index 00000000000..478131538f3 --- /dev/null +++ b/examples/dyn_create_destroy_apps/index.html @@ -0,0 +1,11 @@ + + + + + Yew • Create and destroy apps + + + + + + diff --git a/examples/dyn_create_destroy_apps/index.scss b/examples/dyn_create_destroy_apps/index.scss new file mode 100644 index 00000000000..8aebff88ad9 --- /dev/null +++ b/examples/dyn_create_destroy_apps/index.scss @@ -0,0 +1,27 @@ +button { + border: 0; + color: white; + padding: 14px 14px; + text-align: center; + font-size: 16px; +} + +button.create { + background-color: #008f53; /* Green */ + width: 200px; +} + +button.destroy { + background-color: #ff1f1f; /* Red */ +} + +.counter { + color: #008f53; + font-size: 48px; + text-align: center; +} + +.panel { + display: flex; + justify-content: center; +} diff --git a/examples/dyn_create_destroy_apps/src/counter.rs b/examples/dyn_create_destroy_apps/src/counter.rs new file mode 100644 index 00000000000..32fb497a1ce --- /dev/null +++ b/examples/dyn_create_destroy_apps/src/counter.rs @@ -0,0 +1,78 @@ +use std::time::Duration; +use yew::prelude::*; +use yew_services::{ + interval::{IntervalService, IntervalTask}, + ConsoleService, +}; + +pub struct CounterModel { + counter: usize, + props: CounterProps, + _interval_task: IntervalTask, +} + +#[derive(Clone, Properties)] +pub struct CounterProps { + pub destroy_callback: Callback<()>, +} + +pub enum CounterMessage { + Tick, +} + +impl Component for CounterModel { + type Message = CounterMessage; + + type Properties = CounterProps; + + fn create(props: Self::Properties, link: ComponentLink) -> Self { + // Create a Tick message every second + let interval_task = IntervalService::spawn( + Duration::from_secs(1), + link.callback(|()| Self::Message::Tick), + ); + Self { + counter: 0, + props, + _interval_task: interval_task, + } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + // Count our internal state up by one + Self::Message::Tick => { + self.counter += 1; + true + } + } + } + + fn change(&mut self, _props: Self::Properties) -> ShouldRender { + false + } + + fn view(&self) -> Html { + let destroy_callback = self.props.destroy_callback.clone(); + + html! { + <> + // Display the current value of the counter +

+ { "App has lived for " } + { self.counter } + { " ticks" } +

+ + // Add button to send a destroy command to the parent app + + + } + } + + fn destroy(&mut self) { + ConsoleService::log("CounterModel app destroyed"); + } +} diff --git a/examples/dyn_create_destroy_apps/src/main.rs b/examples/dyn_create_destroy_apps/src/main.rs new file mode 100644 index 00000000000..8c812464ff3 --- /dev/null +++ b/examples/dyn_create_destroy_apps/src/main.rs @@ -0,0 +1,117 @@ +use slab::Slab; +use web_sys::Element; +use yew::prelude::*; +use yew::utils::document; + +mod counter; + +use counter::{CounterModel, CounterProps}; + +// Define the possible messages which can be sent to the component +pub enum Msg { + // Spawns a new instance of the CounterModel app + SpawnCounterAppInstance, + // Destroys an instance of a CounterModel app + DestroyCounterApp(usize), +} + +pub struct Model { + link: ComponentLink, + apps: Slab<(Element, AppHandle)>, // Contains the spawned apps and their parent div elements + apps_container_ref: NodeRef, +} + +impl Component for Model { + type Message = Msg; + type Properties = (); + + fn create(_props: Self::Properties, link: ComponentLink) -> Self { + Self { + link, + apps: Slab::new(), + apps_container_ref: NodeRef::default(), + } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + let app_container = self + .apps_container_ref + .cast::() + .expect("Failed to cast app container div to HTMLElement"); + + match msg { + Msg::SpawnCounterAppInstance => { + // Create a new
HtmlElement where the new app will live + let app_div = document() + .create_element("div") + .expect("Failed to create
element"); + + // Append the div to the document body + let _ = app_container + .append_child(&app_div) + .expect("Failed to append app div app container div"); + + // Reserve an entry for the new app + let app_entry = self.apps.vacant_entry(); + + // Get the key for the entry and create and mount a new CounterModel app + // with a callback that destroys the app when emitted + let app_key = app_entry.key(); + let new_counter_app = yew::start_app_with_props_in_element( + app_div.clone(), + CounterProps { + destroy_callback: self + .link + .callback(move |_| Msg::DestroyCounterApp(app_key)), + }, + ); + + // Insert the app and the app div to our app collection + app_entry.insert((app_div, new_counter_app)); + } + Msg::DestroyCounterApp(app_id) => { + // Get the app from the app slabmap + let (app_div, app) = self.apps.remove(app_id); + + // Destroy the app + app.destroy(); + + // Remove the app div from the DOM + app_div.remove() + } + } + + // Never render + false + } + + fn change(&mut self, _props: Self::Properties) -> ShouldRender { + false + } + + fn view(&self) -> Html { + // We will only render once, and then do the rest of the DOM changes + // by mounting/destroying appinstances of CounterModel + html! { + <> +
+ // Create button to create a new app + +
+ // Create a container for all the app instances +
+
+ + } + } +} + +fn main() { + // Start main app + yew::start_app::(); +} diff --git a/examples/mount_point/src/main.rs b/examples/mount_point/src/main.rs index a0159b6ad4f..d7a39116c35 100644 --- a/examples/mount_point/src/main.rs +++ b/examples/mount_point/src/main.rs @@ -74,5 +74,5 @@ fn main() { body.append_child(&mount_point).unwrap(); - yew::App::::new().mount(mount_point); + yew::start_app_in_element::(mount_point); } diff --git a/examples/two_apps/src/main.rs b/examples/two_apps/src/main.rs index 49a9c0c1065..288ba8580e5 100644 --- a/examples/two_apps/src/main.rs +++ b/examples/two_apps/src/main.rs @@ -1,4 +1,4 @@ -use yew::{html, App, Component, ComponentLink, Html, ShouldRender}; +use yew::{html, AppHandle, Component, ComponentLink, Html, ShouldRender}; pub enum Msg { SetOpposite(ComponentLink), @@ -74,17 +74,16 @@ impl Component for Model { } } -fn mount_app(selector: &'static str, app: App) -> ComponentLink { +fn mount_app(selector: &'static str) -> AppHandle { let document = yew::utils::document(); let element = document.query_selector(selector).unwrap().unwrap(); - app.mount(element) + yew::start_app_in_element(element) } fn main() { - let first_app = App::new(); - let second_app = App::new(); - let to_first = mount_app(".first-app", first_app); - let to_second = mount_app(".second-app", second_app); - to_first.send_message(Msg::SetOpposite(to_second.clone())); - to_second.send_message(Msg::SetOpposite(to_first)); + let first_app = mount_app(".first-app"); + let second_app = mount_app(".second-app"); + + first_app.send_message(Msg::SetOpposite(second_app.clone())); + second_app.send_message(Msg::SetOpposite(first_app.clone())); } diff --git a/packages/yew-functional/tests/lib.rs b/packages/yew-functional/tests/lib.rs index 67b404b6689..babf94f14a7 100644 --- a/packages/yew-functional/tests/lib.rs +++ b/packages/yew-functional/tests/lib.rs @@ -2,7 +2,7 @@ mod common; use common::obtain_result; use wasm_bindgen_test::*; -use yew::{html, App, Html, Properties}; +use yew::{html, Html, Properties}; use yew_functional::{FunctionComponent, FunctionProvider}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -27,8 +27,7 @@ fn props_are_passed() { } } type PropsComponent = FunctionComponent; - let app: App = yew::App::new(); - app.mount_with_props( + yew::start_app_with_props_in_element::( yew::utils::document().get_element_by_id("output").unwrap(), PropsPassedFunctionProps { value: "props".to_string(), diff --git a/packages/yew-functional/tests/use_context.rs b/packages/yew-functional/tests/use_context.rs index 97c6f42ab20..1fdbaaa1bd4 100644 --- a/packages/yew-functional/tests/use_context.rs +++ b/packages/yew-functional/tests/use_context.rs @@ -3,7 +3,7 @@ mod common; use common::obtain_result_by_id; use std::rc::Rc; use wasm_bindgen_test::*; -use yew::{html, App, Children, ContextProvider, Html, Properties}; +use yew::{html, Children, ContextProvider, Html, Properties}; use yew_functional::{ use_context, use_effect, use_ref, use_state, FunctionComponent, FunctionProvider, }; @@ -72,8 +72,9 @@ fn use_context_scoping_works() { } } - let app: App = yew::App::new(); - app.mount(yew::utils::document().get_element_by_id("output").unwrap()); + yew::start_app_in_element::( + yew::utils::document().get_element_by_id("output").unwrap(), + ); let result: String = obtain_result_by_id("result"); assert_eq!("correct", result); } @@ -163,8 +164,9 @@ fn use_context_works_with_multiple_types() { } type TestComponent = FunctionComponent; - let app: App = yew::App::new(); - app.mount(yew::utils::document().get_element_by_id("output").unwrap()); + yew::start_app_in_element::( + yew::utils::document().get_element_by_id("output").unwrap(), + ); } #[wasm_bindgen_test] @@ -273,8 +275,9 @@ fn use_context_update_works() { } type TestComponent = FunctionComponent; - let app: App = yew::App::new(); - app.mount(yew::utils::document().get_element_by_id("output").unwrap()); + yew::start_app_in_element::( + yew::utils::document().get_element_by_id("output").unwrap(), + ); // 1 initial render + 3 update steps assert_eq!(obtain_result_by_id("test-0"), "total: 4"); diff --git a/packages/yew-functional/tests/use_effect.rs b/packages/yew-functional/tests/use_effect.rs index 53414c7a68d..2f211b0c9bb 100644 --- a/packages/yew-functional/tests/use_effect.rs +++ b/packages/yew-functional/tests/use_effect.rs @@ -4,7 +4,7 @@ use common::obtain_result; use std::ops::{Deref, DerefMut}; use std::rc::Rc; use wasm_bindgen_test::*; -use yew::{html, App, Html, Properties}; +use yew::{html, Html, Properties}; use yew_functional::{ use_effect_with_deps, use_ref, use_state, FunctionComponent, FunctionProvider, }; @@ -69,10 +69,9 @@ fn use_effect_destroys_on_component_drop() { } } } - let app: App = yew::App::new(); let destroy_counter = Rc::new(std::cell::RefCell::new(0)); let destroy_counter_c = destroy_counter.clone(); - app.mount_with_props( + yew::start_app_with_props_in_element::( yew::utils::document().get_element_by_id("output").unwrap(), WrapperProps { destroy_called: Rc::new(move || *destroy_counter_c.borrow_mut().deref_mut() += 1), @@ -112,8 +111,9 @@ fn use_effect_works_many_times() { } type UseEffectComponent = FunctionComponent; - let app: App = yew::App::new(); - app.mount(yew::utils::document().get_element_by_id("output").unwrap()); + yew::start_app_in_element::( + yew::utils::document().get_element_by_id("output").unwrap(), + ); let result = obtain_result(); assert_eq!(result.as_str(), "4"); } @@ -146,8 +146,9 @@ fn use_effect_works_once() { } } type UseEffectComponent = FunctionComponent; - let app: App = yew::App::new(); - app.mount(yew::utils::document().get_element_by_id("output").unwrap()); + yew::start_app_in_element::( + yew::utils::document().get_element_by_id("output").unwrap(), + ); let result = obtain_result(); assert_eq!(result.as_str(), "1"); } @@ -193,8 +194,9 @@ fn use_effect_refires_on_dependency_change() { } } type UseEffectComponent = FunctionComponent; - let app: App = yew::App::new(); - app.mount(yew::utils::document().get_element_by_id("output").unwrap()); + yew::start_app_in_element::( + yew::utils::document().get_element_by_id("output").unwrap(), + ); let result: String = obtain_result(); assert_eq!(result.as_str(), "11"); diff --git a/packages/yew-functional/tests/use_reducer.rs b/packages/yew-functional/tests/use_reducer.rs index 41f9301d5fd..4d6c0464fec 100644 --- a/packages/yew-functional/tests/use_reducer.rs +++ b/packages/yew-functional/tests/use_reducer.rs @@ -2,7 +2,7 @@ mod common; use common::obtain_result; use wasm_bindgen_test::*; -use yew::{html, App, Html}; +use yew::{html, Html}; use yew_functional::{ use_effect_with_deps, use_reducer_with_init, FunctionComponent, FunctionProvider, }; @@ -46,8 +46,9 @@ fn use_reducer_works() { } } type UseReducerComponent = FunctionComponent; - let app: App = yew::App::new(); - app.mount(yew::utils::document().get_element_by_id("output").unwrap()); + yew::start_app_in_element::( + yew::utils::document().get_element_by_id("output").unwrap(), + ); let result = obtain_result(); assert_eq!(result.as_str(), "11"); diff --git a/packages/yew-functional/tests/use_ref.rs b/packages/yew-functional/tests/use_ref.rs index c562b226fc9..61023d044f9 100644 --- a/packages/yew-functional/tests/use_ref.rs +++ b/packages/yew-functional/tests/use_ref.rs @@ -3,7 +3,7 @@ mod common; use common::obtain_result; use std::ops::DerefMut; use wasm_bindgen_test::*; -use yew::{html, App, Html}; +use yew::{html, Html}; use yew_functional::{use_ref, use_state, FunctionComponent, FunctionProvider}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -31,8 +31,9 @@ fn use_ref_works() { } } type UseRefComponent = FunctionComponent; - let app: App = yew::App::new(); - app.mount(yew::utils::document().get_element_by_id("output").unwrap()); + yew::start_app_in_element::( + yew::utils::document().get_element_by_id("output").unwrap(), + ); let result = obtain_result(); assert_eq!(result.as_str(), "true"); diff --git a/packages/yew-functional/tests/use_state.rs b/packages/yew-functional/tests/use_state.rs index 113537ba9f5..c4e259a1327 100644 --- a/packages/yew-functional/tests/use_state.rs +++ b/packages/yew-functional/tests/use_state.rs @@ -2,7 +2,7 @@ mod common; use common::obtain_result; use wasm_bindgen_test::*; -use yew::{html, App, Html}; +use yew::{html, Html}; use yew_functional::{use_effect_with_deps, use_state, FunctionComponent, FunctionProvider}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -28,8 +28,9 @@ fn use_state_works() { } } type UseComponent = FunctionComponent; - let app: App = yew::App::new(); - app.mount(yew::utils::document().get_element_by_id("output").unwrap()); + yew::start_app_in_element::( + yew::utils::document().get_element_by_id("output").unwrap(), + ); let result = obtain_result(); assert_eq!(result.as_str(), "5"); } @@ -72,8 +73,9 @@ fn multiple_use_state_setters() { } } type UseComponent = FunctionComponent; - let app: App = yew::App::new(); - app.mount(yew::utils::document().get_element_by_id("output").unwrap()); + yew::start_app_in_element::( + yew::utils::document().get_element_by_id("output").unwrap(), + ); let result = obtain_result(); assert_eq!(result.as_str(), "11"); } diff --git a/packages/yew/src/app.rs b/packages/yew/src/app.rs deleted file mode 100644 index a2647eaa58b..00000000000 --- a/packages/yew/src/app.rs +++ /dev/null @@ -1,140 +0,0 @@ -//! This module contains the `App` struct, which is used to bootstrap -//! a component in an isolated scope. - -use crate::html::{Component, ComponentLink, NodeRef, Scope}; -use crate::utils::document; -use web_sys::Element; - -/// An instance of an application. -#[derive(Debug)] -pub struct App { - /// `Scope` holder - scope: Scope, -} - -impl Default for App -where - COMP: Component, -{ - fn default() -> Self { - App::new() - } -} - -impl App -where - COMP: Component, - COMP::Properties: Default, -{ - /// The main entry point of a Yew program. It works similarly to the `program` - /// function in Elm. You should provide an initial model, `update` function - /// which will update the state of the model and a `view` function which - /// will render the model to a virtual DOM tree. If you would like to pass props, - /// use the `mount_with_props` method. - pub fn mount(self, element: Element) -> ComponentLink { - clear_element(&element); - self.scope.mount_in_place( - element, - NodeRef::default(), - NodeRef::default(), - COMP::Properties::default(), - ) - } - - /// Alias to `mount("body", ...)`. - pub fn mount_to_body(self) -> ComponentLink { - // Bootstrap the component for `Window` environment only (not for `Worker`) - let element = document() - .query_selector("body") - .expect("can't get body node for rendering") - .expect("can't unwrap body node"); - self.mount(element) - } - - /// Alternative to `mount` which replaces the body element with a component which has a body - /// element at the root of the HTML generated by its `view` method. Use this method when you - /// need to manipulate the body element. For example, adding/removing app-wide - /// CSS classes of the body element. - pub fn mount_as_body(self) -> ComponentLink { - let html_element = document() - .query_selector("html") - .expect("can't get html node for rendering") - .expect("can't unwrap html node"); - let body_element = document() - .query_selector("body") - .expect("can't get body node for rendering") - .expect("can't unwrap body node"); - html_element - .remove_child(&body_element) - .expect("can't remove body child"); - self.scope.mount_in_place( - html_element, - NodeRef::default(), - NodeRef::default(), - COMP::Properties::default(), - ) - } -} - -impl App -where - COMP: Component, -{ - /// Creates a new `App` with a component in a context. - pub fn new() -> Self { - std::panic::set_hook(Box::new(console_error_panic_hook::hook)); - let scope = Scope::new(None); - App { scope } - } - - /// 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` - /// function which will update the state of the model and a `view` function which - /// will render the model to a virtual DOM tree. - pub fn mount_with_props( - self, - element: Element, - props: COMP::Properties, - ) -> ComponentLink { - clear_element(&element); - self.scope - .mount_in_place(element, NodeRef::default(), NodeRef::default(), props) - } - - /// Alias to `mount_with_props("body", ...)`. - pub fn mount_to_body_with_props(self, props: COMP::Properties) -> ComponentLink { - // Bootstrap the component for `Window` environment only (not for `Worker`) - let element = document() - .query_selector("body") - .expect("can't get body node for rendering") - .expect("can't unwrap body node"); - self.mount_with_props(element, props) - } - - /// Alternative to `mount_with_props` which replaces the body element with a component which - /// has a body element at the root of the HTML generated by its `view` method. Use this method - /// when you need to manipulate the body element. For example, adding/removing app-wide - /// CSS classes of the body element. - pub fn mount_as_body_with_props(self, props: COMP::Properties) -> ComponentLink { - let html_element = document() - .query_selector("html") - .expect("can't get html node for rendering") - .expect("can't unwrap html node"); - let body_element = document() - .query_selector("body") - .expect("can't get body node for rendering") - .expect("can't unwrap body node"); - html_element - .remove_child(&body_element) - .expect("can't remove body child"); - self.scope - .mount_in_place(html_element, NodeRef::default(), NodeRef::default(), props) - } -} - -/// Removes anything from the given element. -fn clear_element(element: &Element) { - while let Some(child) = element.last_child() { - element.remove_child(&child).expect("can't remove a child"); - } -} diff --git a/packages/yew/src/app_handle.rs b/packages/yew/src/app_handle.rs new file mode 100644 index 00000000000..0899939b4f4 --- /dev/null +++ b/packages/yew/src/app_handle.rs @@ -0,0 +1,72 @@ +//! This module contains the `App` struct, which is used to bootstrap +//! a component in an isolated scope. + +use std::ops::Deref; + +use crate::html::{Component, NodeRef, Scope, Scoped}; +use crate::utils::document; +use web_sys::Element; + +/// An instance of an application. +#[derive(Debug)] +pub struct AppHandle { + /// `Scope` holder + pub(crate) scope: Scope, +} + +impl AppHandle +where + COMP: Component, +{ + /// 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` + /// function which will update the state of the model and a `view` function which + /// will render the model to a virtual DOM tree. + pub(crate) fn mount_with_props(element: Element, props: COMP::Properties) -> Self { + clear_element(&element); + let app = Self { + scope: Scope::new(None), + }; + app.scope + .mount_in_place(element, NodeRef::default(), NodeRef::default(), props); + + app + } + + /// Alternative to `mount_with_props` which replaces the body element with a component which + /// has a body element at the root of the HTML generated by its `view` method. Use this method + /// when you need to manipulate the body element. For example, adding/removing app-wide + /// CSS classes of the body element. + pub(crate) fn mount_as_body_with_props(props: COMP::Properties) -> Self { + let html_element = document().document_element().unwrap(); + let body_element = document().body().expect("no body node found"); + html_element + .remove_child(&body_element) + .expect("can't remove body child"); + + Self::mount_with_props(html_element, props) + } + + /// Schedule the app for destruction + pub fn destroy(mut self) { + self.scope.destroy() + } +} + +impl Deref for AppHandle +where + COMP: Component, +{ + type Target = Scope; + + fn deref(&self) -> &Self::Target { + &self.scope + } +} + +/// Removes anything from the given element. +fn clear_element(element: &Element) { + while let Some(child) = element.last_child() { + element.remove_child(&child).expect("can't remove a child"); + } +} diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 6d25ccb0a2f..8fed6e9258f 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -162,12 +162,12 @@ impl Scope { /// Mounts a component with `props` to the specified `element` in the DOM. pub(crate) fn mount_in_place( - self, + &self, parent: Element, next_sibling: NodeRef, node_ref: NodeRef, props: COMP::Properties, - ) -> Scope { + ) { let placeholder = { let placeholder: Node = document().create_text_node("").into(); insert_node(&placeholder, &parent, next_sibling.get()); @@ -184,8 +184,6 @@ impl Scope { props, scope: self.clone(), })); - - self } pub(crate) fn reuse(&self, props: COMP::Properties, node_ref: NodeRef, next_sibling: NodeRef) { diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index d830a7d8d80..90cd88dc45f 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -86,6 +86,8 @@ #![recursion_limit = "512"] extern crate self as yew; +use std::{cell::Cell, panic::PanicInfo}; + /// This macro provides a convenient way to create [`Classes`]. /// /// The macro takes a list of items similar to the [`vec!`] macro and returns a [`Classes`] instance. @@ -268,7 +270,7 @@ pub mod macros { pub use crate::props; } -pub mod app; +mod app_handle; pub mod callback; pub mod context; pub mod format; @@ -293,21 +295,99 @@ pub mod events { }; } -/// Starts an app mounted to a body of the document. -pub fn start_app() +pub use crate::app_handle::AppHandle; +use web_sys::Element; + +thread_local! { + static PANIC_HOOK_IS_SET: Cell = Cell::new(false); +} + +/// Set a custom panic hook. +/// Unless a panic hook is set through this function, Yew will +/// overwrite any existing panic hook when one of the `start_app*` functions are called. +pub fn set_custom_panic_hook(hook: Box) + Sync + Send + 'static>) { + std::panic::set_hook(hook); + PANIC_HOOK_IS_SET.with(|hook_is_set| hook_is_set.set(true)); +} + +fn set_default_panic_hook() { + if !PANIC_HOOK_IS_SET.with(|hook_is_set| hook_is_set.replace(true)) { + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); + } +} + +/// The main entry point of a Yew application. +/// If you would like to pass props, use the `start_app_with_props_in_element` method. +pub fn start_app_in_element(element: Element) -> AppHandle +where + COMP: Component, + COMP::Properties: Default, +{ + start_app_with_props_in_element(element, COMP::Properties::default()) +} + +/// Starts an yew app mounted to the body of the document. +/// Alias to start_app_in_element(Body) +pub fn start_app() -> AppHandle where COMP: Component, COMP::Properties: Default, { - App::::new().mount_to_body(); + start_app_with_props(COMP::Properties::default()) +} + +/// The main entry point of a Yew application. +/// Alternative to `start_app` which replaces the body element with a component which has a body +/// element at the root of the HTML generated by its `view` method. Use this method when you +/// need to manipulate the body element. For example, adding/removing app-wide +/// CSS classes of the body element. +pub fn start_app_as_body() -> AppHandle +where + COMP: Component, + COMP::Properties: Default, +{ + start_app_with_props_as_body(COMP::Properties::default()) +} + +/// The main entry point of a Yew application. This function does the +/// same as `start_app_in_element(...)` but allows to start an Yew application with properties. +pub fn start_app_with_props_in_element( + element: Element, + props: COMP::Properties, +) -> AppHandle +where + COMP: Component, +{ + set_default_panic_hook(); + AppHandle::::mount_with_props(element, props) +} + +/// The main entry point of a Yew application. +/// This function does the same as `start_app(...)` but allows to start an Yew application with properties. +pub fn start_app_with_props(props: COMP::Properties) -> AppHandle +where + COMP: Component, +{ + start_app_with_props_in_element( + crate::utils::document() + .body() + .expect("no body node found") + .into(), + props, + ) } -/// Starts an app mounted to a body of the document. -pub fn start_app_with_props(props: COMP::Properties) +/// The main entry point of a Yew application. +/// Alternative to `start_app_with_props` which replaces the body element with a component which has a body +/// element at the root of the HTML generated by its `view` method. Use this method when you +/// need to manipulate the body element. For example, adding/removing app-wide +/// CSS classes of the body element. +pub fn start_app_with_props_as_body(props: COMP::Properties) -> AppHandle where COMP: Component, { - App::::new().mount_to_body_with_props(props); + set_default_panic_hook(); + AppHandle::::mount_as_body_with_props(props) } /// The Yew Prelude @@ -321,7 +401,7 @@ where pub mod prelude { #[cfg(feature = "agent")] pub use crate::agent::{Bridge, Bridged, Dispatched, Threaded}; - pub use crate::app::App; + pub use crate::app_handle::AppHandle; pub use crate::callback::Callback; pub use crate::context::ContextProvider; pub use crate::events::*; diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index ae0c51fb383..c72346108b9 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -143,7 +143,7 @@ impl Mountable for PropsWrapper { next_sibling: NodeRef, ) -> Box { let scope: Scope = Scope::new(Some(parent_scope.clone())); - let scope = scope.mount_in_place(parent, next_sibling, node_ref, self.props); + scope.mount_in_place(parent, next_sibling, node_ref, self.props); Box::new(scope) }