Change the app struct to be a real handle to an Yew app instance and make it possible to destroy a running app (#1825)

* Naive implementation of destroy app

* Move functions get_component_link and destroy

The function get_component_link and destroy on App does not require
Component::Properties to be Default. So move them to their own impl
block.

* Change the fn signatures for the other mount fns too

* Add example of how to use the apps dynamically

* Rename example

* Change title of example html file

* Update examples/dyn_create_destroy_apps/src/counter.rs

Co-authored-by: Simon <simon@siku2.io>

* Change fn signatures of App to not take Self

* app.rs thorugh to public API: only make it possible to create an
instance of an App by mounting it. And only make it possible to mount
an App by creating it. This prevents double mounting.
* Change Scope::mount_in_place to take &self instead of self, because it
doesn't have too.

* Fix examples to compile with new App signature

* Fix tests to compile with new App signature

* Remove pub pulic App::mount* API

* Make the documentation compile again with the new start_app API

* Make the examples compile again with the new start_app API

* Make the yew packages compile again with the new start_app API

* rename module yew::app to yew::app_handle

* Fix identation in styling file for dyn_create_destroy_app ex

* Fix naming in examples/dyn_create_destroy_apps/README.md

Co-authored-by: Simon <simon@siku2.io>

* Use Self instead of AppHandle to create AppHandle

Co-authored-by: Simon <simon@siku2.io>

* Fix the start_app_in_body docs

Co-authored-by: Simon <simon@siku2.io>

* Remove comparison with Elm in the start_app docs

Co-authored-by: Simon <simon@siku2.io>

* Fix english in dyn_create_destroy_apps example

Co-authored-by: Simon <simon@siku2.io>

* Impl Deref instead of AsRef for AppHandle

* Formatting

* Remove AppHandle::new use Default trait instead

* Rename the Yew::start_app* function names

* Revert "Remove AppHandle::new use Default trait instead"

This reverts commit 97e4897c1edafac34e012f13bc2b5084f68f8dce.

* remove default impl for AppHandle

* Make the tests compile again

* Make the examples compile again

* Add dyn_create_destroy_apps to examples/README.md

* Remove mount_to_body_with_props

Co-authored-by: Simon <simon@siku2.io>

* Use yew getters instead of query selectors

Co-authored-by: Simon <simon@siku2.io>

* Remove AppHandle::new

* Remove unused function

* code style fix

* Fix compile error

* Add func set_custom_panic_hook

This commit adds the function set_custom_panic_hook. In addition Yew
will now check if a custom panic is registered when one of start_app*
functions are called, if that is not the case a default panic hook is
registered.

* Fix docs for set_custom_panic_hook

Co-authored-by: Simon <simon@siku2.io>

Co-authored-by: Nicklas Warming Jacobsen <nwj@skybox.gg>
Co-authored-by: Simon <simon@siku2.io>
This commit is contained in:
Nicklas Warming Jacobsen 2021-05-17 13:43:06 +02:00 committed by GitHub
parent 0bd3b759c6
commit 09a41d6903
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 511 additions and 216 deletions

View File

@ -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",

View File

@ -26,29 +26,30 @@ As an example, check out the TodoMVC example here: <https://examples.yew.rs/todo
## List of examples
| Example | Description |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| [boids](boids) | Yew port of [Boids](https://en.wikipedia.org/wiki/Boids) |
| [counter](counter) | Simple counter which can be incremented and decremented |
| [crm](crm) | Shallow customer relationship management tool |
| [dashboard](dashboard) | Uses the `fetch` and `websocket` services to load external data |
| [file_upload](file_upload) | Uses the `reader` service to read the content of user uploaded files |
| [futures](futures) | Demonstrates how you can use futures and async code with Yew. Features a Markdown renderer. |
| [game_of_life](game_of_life) | Implementation of [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) |
| [inner_html](inner_html) | Embeds an external document as raw HTML by manually managing the element |
| [js_callback](js_callback) | Interacts with JavaScript code |
| [keyed_list](keyed_list) | Demonstrates how to use keys to improve the performance of lists |
| [mount_point](mount_point) | Shows how to mount the root component to a custom element |
| [multi_thread](multi_thread) | Demonstrates the use of Web Workers to offload computation to the background |
| [nested_list](nested_list) | Renders a styled list which tracks hover events |
| [node_refs](node_refs) | Uses a [`NodeRef`](https://yew.rs/docs/concepts/components/refs) to focus the input element under the cursor |
| [pub_sub](pub_sub) | Cross-component communication using [Agents](https://yew.rs/docs/concepts/agents) |
| [router](router) | The best yew blog built with `yew-router` |
| [store](store) | Showcases the `yewtil::store` API |
| [timer](timer) | Demonstrates the use of the interval and timeout services |
| [todomvc](todomvc) | Implementation of [TodoMVC](http://todomvc.com/) |
| [two_apps](two_apps) | Runs two separate Yew apps which can communicate with each other |
| [webgl](webgl) | Controls a [WebGL canvas](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Getting_started_with_WebGL) from Yew |
| Example | Description |
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| [boids](boids) | Yew port of [Boids](https://en.wikipedia.org/wiki/Boids) |
| [counter](counter) | Simple counter which can be incremented and decremented |
| [crm](crm) | Shallow customer relationship management tool |
| [dashboard](dashboard) | Uses the `fetch` and `websocket` services to load external data |
| [dyn_create_destroy_apps](dyn_create_destroy_apps) | Uses the function `start_app_in_element` and the `AppHandle` struct to dynamically create and delete Yew apps |
| [file_upload](file_upload) | Uses the `reader` service to read the content of user uploaded files |
| [futures](futures) | Demonstrates how you can use futures and async code with Yew. Features a Markdown renderer. |
| [game_of_life](game_of_life) | Implementation of [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) |
| [inner_html](inner_html) | Embeds an external document as raw HTML by manually managing the element |
| [js_callback](js_callback) | Interacts with JavaScript code |
| [keyed_list](keyed_list) | Demonstrates how to use keys to improve the performance of lists |
| [mount_point](mount_point) | Shows how to mount the root component to a custom element |
| [multi_thread](multi_thread) | Demonstrates the use of Web Workers to offload computation to the background |
| [nested_list](nested_list) | Renders a styled list which tracks hover events |
| [node_refs](node_refs) | Uses a [`NodeRef`](https://yew.rs/docs/concepts/components/refs) to focus the input element under the cursor |
| [pub_sub](pub_sub) | Cross-component communication using [Agents](https://yew.rs/docs/concepts/agents) |
| [router](router) | The best yew blog built with `yew-router` |
| [store](store) | Showcases the `yewtil::store` API |
| [timer](timer) | Demonstrates the use of the interval and timeout services |
| [todomvc](todomvc) | Implementation of [TodoMVC](http://todomvc.com/) |
| [two_apps](two_apps) | Runs two separate Yew apps which can communicate with each other |
| [webgl](webgl) | Controls a [WebGL canvas](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Getting_started_with_WebGL) from Yew |
## Next steps

View File

@ -0,0 +1,22 @@
[package]
name = "dyn_create_destroy_apps"
version = "0.1.0"
authors = ["Nicklas Warming Jacobsen <nicklaswj@gmail.com>"]
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"
]

View File

@ -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.

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Yew • Create and destroy apps</title>
<link data-trunk rel="sass" href="index.scss" />
</head>
<body></body>
</html>

View File

@ -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;
}

View File

@ -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>) -> 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
<p class="counter">
{ "App has lived for " }
{ self.counter }
{ " ticks" }
</p>
// Add button to send a destroy command to the parent app
<button class="destroy" onclick=Callback::from(move |_| destroy_callback.emit(()))>
{ "Destroy this app" }
</button>
</>
}
}
fn destroy(&mut self) {
ConsoleService::log("CounterModel app destroyed");
}
}

View File

@ -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<Self>,
apps: Slab<(Element, AppHandle<CounterModel>)>, // 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 {
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::<Element>()
.expect("Failed to cast app container div to HTMLElement");
match msg {
Msg::SpawnCounterAppInstance => {
// Create a new <div> HtmlElement where the new app will live
let app_div = document()
.create_element("div")
.expect("Failed to create <div> 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! {
<>
<div class="panel">
// Create button to create a new app
<button
class="create"
onclick=self.link.callback(|_| Msg::SpawnCounterAppInstance)
>
{ "Spawn new CounterModel app" }
</button>
</div>
// Create a container for all the app instances
<div ref=self.apps_container_ref.clone()>
</div>
</>
}
}
}
fn main() {
// Start main app
yew::start_app::<Model>();
}

View File

@ -74,5 +74,5 @@ fn main() {
body.append_child(&mount_point).unwrap();
yew::App::<Model>::new().mount(mount_point);
yew::start_app_in_element::<Model>(mount_point);
}

View File

@ -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<Model>),
@ -74,17 +74,16 @@ impl Component for Model {
}
}
fn mount_app(selector: &'static str, app: App<Model>) -> ComponentLink<Model> {
fn mount_app(selector: &'static str) -> AppHandle<Model> {
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()));
}

View File

@ -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<PropsPassedFunction>;
let app: App<PropsComponent> = yew::App::new();
app.mount_with_props(
yew::start_app_with_props_in_element::<PropsComponent>(
yew::utils::document().get_element_by_id("output").unwrap(),
PropsPassedFunctionProps {
value: "props".to_string(),

View File

@ -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<UseContextComponent> = yew::App::new();
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
yew::start_app_in_element::<UseContextComponent>(
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<TestFunction>;
let app: App<TestComponent> = yew::App::new();
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
yew::start_app_in_element::<TestComponent>(
yew::utils::document().get_element_by_id("output").unwrap(),
);
}
#[wasm_bindgen_test]
@ -273,8 +275,9 @@ fn use_context_update_works() {
}
type TestComponent = FunctionComponent<TestFunction>;
let app: App<TestComponent> = yew::App::new();
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
yew::start_app_in_element::<TestComponent>(
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");

View File

@ -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<UseEffectWrapperComponent> = 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::<UseEffectWrapperComponent>(
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<UseEffectFunction>;
let app: App<UseEffectComponent> = yew::App::new();
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
yew::start_app_in_element::<UseEffectComponent>(
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<UseEffectFunction>;
let app: App<UseEffectComponent> = yew::App::new();
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
yew::start_app_in_element::<UseEffectComponent>(
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<UseEffectFunction>;
let app: App<UseEffectComponent> = yew::App::new();
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
yew::start_app_in_element::<UseEffectComponent>(
yew::utils::document().get_element_by_id("output").unwrap(),
);
let result: String = obtain_result();
assert_eq!(result.as_str(), "11");

View File

@ -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<UseReducerFunction>;
let app: App<UseReducerComponent> = yew::App::new();
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
yew::start_app_in_element::<UseReducerComponent>(
yew::utils::document().get_element_by_id("output").unwrap(),
);
let result = obtain_result();
assert_eq!(result.as_str(), "11");

View File

@ -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<UseRefFunction>;
let app: App<UseRefComponent> = yew::App::new();
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
yew::start_app_in_element::<UseRefComponent>(
yew::utils::document().get_element_by_id("output").unwrap(),
);
let result = obtain_result();
assert_eq!(result.as_str(), "true");

View File

@ -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<UseStateFunction>;
let app: App<UseComponent> = yew::App::new();
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
yew::start_app_in_element::<UseComponent>(
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<UseStateFunction>;
let app: App<UseComponent> = yew::App::new();
app.mount(yew::utils::document().get_element_by_id("output").unwrap());
yew::start_app_in_element::<UseComponent>(
yew::utils::document().get_element_by_id("output").unwrap(),
);
let result = obtain_result();
assert_eq!(result.as_str(), "11");
}

View File

@ -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<COMP: Component> {
/// `Scope` holder
scope: Scope<COMP>,
}
impl<COMP> Default for App<COMP>
where
COMP: Component,
{
fn default() -> Self {
App::new()
}
}
impl<COMP> App<COMP>
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<COMP> {
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<COMP> {
// 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<COMP> {
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<COMP> App<COMP>
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<COMP> {
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<COMP> {
// 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<COMP> {
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");
}
}

View File

@ -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<COMP: Component> {
/// `Scope` holder
pub(crate) scope: Scope<COMP>,
}
impl<COMP> AppHandle<COMP>
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<COMP> Deref for AppHandle<COMP>
where
COMP: Component,
{
type Target = Scope<COMP>;
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");
}
}

View File

@ -162,12 +162,12 @@ impl<COMP: Component> Scope<COMP> {
/// 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<COMP> {
) {
let placeholder = {
let placeholder: Node = document().create_text_node("").into();
insert_node(&placeholder, &parent, next_sibling.get());
@ -184,8 +184,6 @@ impl<COMP: Component> Scope<COMP> {
props,
scope: self.clone(),
}));
self
}
pub(crate) fn reuse(&self, props: COMP::Properties, node_ref: NodeRef, next_sibling: NodeRef) {

View File

@ -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<COMP>()
pub use crate::app_handle::AppHandle;
use web_sys::Element;
thread_local! {
static PANIC_HOOK_IS_SET: Cell<bool> = 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<dyn Fn(&PanicInfo<'_>) + 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<COMP>(element: Element) -> AppHandle<COMP>
where
COMP: Component,
COMP::Properties: Default,
{
App::<COMP>::new().mount_to_body();
start_app_with_props_in_element(element, COMP::Properties::default())
}
/// Starts an app mounted to a body of the document.
pub fn start_app_with_props<COMP>(props: COMP::Properties)
/// Starts an yew app mounted to the body of the document.
/// Alias to start_app_in_element(Body)
pub fn start_app<COMP>() -> AppHandle<COMP>
where
COMP: Component,
COMP::Properties: Default,
{
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<COMP>() -> AppHandle<COMP>
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<COMP>(
element: Element,
props: COMP::Properties,
) -> AppHandle<COMP>
where
COMP: Component,
{
App::<COMP>::new().mount_to_body_with_props(props);
set_default_panic_hook();
AppHandle::<COMP>::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<COMP>(props: COMP::Properties) -> AppHandle<COMP>
where
COMP: Component,
{
start_app_with_props_in_element(
crate::utils::document()
.body()
.expect("no body node found")
.into(),
props,
)
}
/// 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<COMP>(props: COMP::Properties) -> AppHandle<COMP>
where
COMP: Component,
{
set_default_panic_hook();
AppHandle::<COMP>::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::*;

View File

@ -143,7 +143,7 @@ impl<COMP: Component> Mountable for PropsWrapper<COMP> {
next_sibling: NodeRef,
) -> Box<dyn Scoped> {
let scope: Scope<COMP> = 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)
}