mirror of
https://github.com/yewstack/yew.git
synced 2026-02-01 17:26:28 +00:00
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:
parent
0bd3b759c6
commit
09a41d6903
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
22
examples/dyn_create_destroy_apps/Cargo.toml
Normal file
22
examples/dyn_create_destroy_apps/Cargo.toml
Normal 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"
|
||||
]
|
||||
21
examples/dyn_create_destroy_apps/README.md
Normal file
21
examples/dyn_create_destroy_apps/README.md
Normal 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.
|
||||
11
examples/dyn_create_destroy_apps/index.html
Normal file
11
examples/dyn_create_destroy_apps/index.html
Normal 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>
|
||||
27
examples/dyn_create_destroy_apps/index.scss
Normal file
27
examples/dyn_create_destroy_apps/index.scss
Normal 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;
|
||||
}
|
||||
78
examples/dyn_create_destroy_apps/src/counter.rs
Normal file
78
examples/dyn_create_destroy_apps/src/counter.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
117
examples/dyn_create_destroy_apps/src/main.rs
Normal file
117
examples/dyn_create_destroy_apps/src/main.rs
Normal 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>();
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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()));
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
72
packages/yew/src/app_handle.rs
Normal file
72
packages/yew/src/app_handle.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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::*;
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user