mirror of https://github.com/linebender/xilem
xilem_web: Add an `AfterBuild`, `AfterRebuild` and `BeforeTeardown` view, which reflects `Ref` in React (#481)
Co-authored-by: Philipp Mildenberger <philipp@mildenberger.me>
This commit is contained in:
parent
2eda5a2a50
commit
5b6ebc324f
|
@ -2649,6 +2649,17 @@ version = "0.6.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "raw_dom_access"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"console_log",
|
||||||
|
"log",
|
||||||
|
"web-sys",
|
||||||
|
"xilem_web",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "read-fonts"
|
name = "read-fonts"
|
||||||
version = "0.19.3"
|
version = "0.19.3"
|
||||||
|
|
|
@ -12,6 +12,7 @@ members = [
|
||||||
"xilem_web/web_examples/fetch",
|
"xilem_web/web_examples/fetch",
|
||||||
"xilem_web/web_examples/todomvc",
|
"xilem_web/web_examples/todomvc",
|
||||||
"xilem_web/web_examples/mathml_svg",
|
"xilem_web/web_examples/mathml_svg",
|
||||||
|
"xilem_web/web_examples/raw_dom_access",
|
||||||
"xilem_web/web_examples/spawn_tasks",
|
"xilem_web/web_examples/spawn_tasks",
|
||||||
"xilem_web/web_examples/svgtoy",
|
"xilem_web/web_examples/svgtoy",
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,254 @@
|
||||||
|
// Copyright 2023 the Xilem Authors
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
use xilem_core::{MessageResult, Mut, View, ViewId, ViewMarker};
|
||||||
|
|
||||||
|
use crate::{DomNode, DomView, DynMessage, ViewCtx};
|
||||||
|
|
||||||
|
pub struct AfterBuild<State, Action, E, F> {
|
||||||
|
element: E,
|
||||||
|
callback: F,
|
||||||
|
phantom: PhantomData<fn() -> (State, Action)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AfterRebuild<State, Action, E, F> {
|
||||||
|
element: E,
|
||||||
|
callback: F,
|
||||||
|
phantom: PhantomData<fn() -> (State, Action)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BeforeTeardown<State, Action, E, F> {
|
||||||
|
element: E,
|
||||||
|
callback: F,
|
||||||
|
phantom: PhantomData<fn() -> (State, Action)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invokes the `callback` after the inner `element` [`DomView`] was created.
|
||||||
|
/// The callback has a reference to the raw DOM node as its only parameter.
|
||||||
|
///
|
||||||
|
/// Caution: At this point, however,
|
||||||
|
/// no properties have been applied to the node.
|
||||||
|
///
|
||||||
|
/// As accessing the underlying raw DOM node can mess with the inner logic of `xilem_web`,
|
||||||
|
/// this should only be used as an escape-hatch for properties not supported by `xilem_web`.
|
||||||
|
/// E.g. to be interoperable with external javascript libraries.
|
||||||
|
pub fn after_build<State, Action, E, F>(element: E, callback: F) -> AfterBuild<State, Action, E, F>
|
||||||
|
where
|
||||||
|
State: 'static,
|
||||||
|
Action: 'static,
|
||||||
|
E: DomView<State, Action> + 'static,
|
||||||
|
F: Fn(&E::DomNode) + 'static,
|
||||||
|
{
|
||||||
|
AfterBuild {
|
||||||
|
element,
|
||||||
|
callback,
|
||||||
|
phantom: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invokes the `callback` after the inner `element` [`DomView<State>`]
|
||||||
|
/// was rebuild, which usually happens after anything has changed in the `State` .
|
||||||
|
///
|
||||||
|
/// Memoization can prevent `callback` being called.
|
||||||
|
/// The callback has a reference to the raw DOM node as its only parameter.
|
||||||
|
///
|
||||||
|
/// As accessing the underlying raw DOM node can mess with the inner logic of `xilem_web`,
|
||||||
|
/// this should only be used as an escape-hatch for properties not supported by `xilem_web`.
|
||||||
|
/// E.g. to be interoperable with external javascript libraries.
|
||||||
|
pub fn after_rebuild<State, Action, E, F>(
|
||||||
|
element: E,
|
||||||
|
callback: F,
|
||||||
|
) -> AfterRebuild<State, Action, E, F>
|
||||||
|
where
|
||||||
|
State: 'static,
|
||||||
|
Action: 'static,
|
||||||
|
E: DomView<State, Action> + 'static,
|
||||||
|
F: Fn(&E::DomNode) + 'static,
|
||||||
|
{
|
||||||
|
AfterRebuild {
|
||||||
|
element,
|
||||||
|
callback,
|
||||||
|
phantom: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invokes the `callback` before the inner `element` [`DomView`] (and its underlying DOM node) is destroyed.
|
||||||
|
///
|
||||||
|
/// As accessing the underlying raw DOM node can mess with the inner logic of `xilem_web`,
|
||||||
|
/// this should only be used as an escape-hatch for properties not supported by `xilem_web`.
|
||||||
|
/// E.g. to be interoperable with external javascript libraries.
|
||||||
|
pub fn before_teardown<State, Action, E, F>(
|
||||||
|
element: E,
|
||||||
|
callback: F,
|
||||||
|
) -> BeforeTeardown<State, Action, E, F>
|
||||||
|
where
|
||||||
|
State: 'static,
|
||||||
|
Action: 'static,
|
||||||
|
E: DomView<State, Action> + 'static,
|
||||||
|
F: Fn(&E::DomNode) + 'static,
|
||||||
|
{
|
||||||
|
BeforeTeardown {
|
||||||
|
element,
|
||||||
|
callback,
|
||||||
|
phantom: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State, Action, E, F> ViewMarker for AfterBuild<State, Action, E, F> {}
|
||||||
|
impl<State, Action, E, F> ViewMarker for AfterRebuild<State, Action, E, F> {}
|
||||||
|
impl<State, Action, E, F> ViewMarker for BeforeTeardown<State, Action, E, F> {}
|
||||||
|
|
||||||
|
impl<State, Action, V, F> View<State, Action, ViewCtx, DynMessage>
|
||||||
|
for AfterBuild<State, Action, V, F>
|
||||||
|
where
|
||||||
|
State: 'static,
|
||||||
|
Action: 'static,
|
||||||
|
F: Fn(&V::DomNode) + 'static,
|
||||||
|
V: DomView<State, Action> + 'static,
|
||||||
|
{
|
||||||
|
type Element = V::Element;
|
||||||
|
|
||||||
|
type ViewState = V::ViewState;
|
||||||
|
|
||||||
|
fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
|
||||||
|
let (el, view_state) = self.element.build(ctx);
|
||||||
|
// TODO:
|
||||||
|
// The props should be applied before the callback is invoked.
|
||||||
|
(self.callback)(&el.node);
|
||||||
|
(el, view_state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild<'el>(
|
||||||
|
&self,
|
||||||
|
prev: &Self,
|
||||||
|
view_state: &mut Self::ViewState,
|
||||||
|
ctx: &mut ViewCtx,
|
||||||
|
element: Mut<'el, Self::Element>,
|
||||||
|
) -> Mut<'el, Self::Element> {
|
||||||
|
self.element
|
||||||
|
.rebuild(&prev.element, view_state, ctx, element)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn teardown(
|
||||||
|
&self,
|
||||||
|
view_state: &mut Self::ViewState,
|
||||||
|
ctx: &mut ViewCtx,
|
||||||
|
el: Mut<'_, Self::Element>,
|
||||||
|
) {
|
||||||
|
self.element.teardown(view_state, ctx, el);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn message(
|
||||||
|
&self,
|
||||||
|
view_state: &mut Self::ViewState,
|
||||||
|
id_path: &[ViewId],
|
||||||
|
message: DynMessage,
|
||||||
|
app_state: &mut State,
|
||||||
|
) -> MessageResult<Action, DynMessage> {
|
||||||
|
self.element
|
||||||
|
.message(view_state, id_path, message, app_state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State, Action, V, F> View<State, Action, ViewCtx, DynMessage>
|
||||||
|
for AfterRebuild<State, Action, V, F>
|
||||||
|
where
|
||||||
|
State: 'static,
|
||||||
|
Action: 'static,
|
||||||
|
F: Fn(&V::DomNode) + 'static,
|
||||||
|
V: DomView<State, Action> + 'static,
|
||||||
|
{
|
||||||
|
type Element = V::Element;
|
||||||
|
|
||||||
|
type ViewState = V::ViewState;
|
||||||
|
|
||||||
|
fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
|
||||||
|
self.element.build(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild<'el>(
|
||||||
|
&self,
|
||||||
|
prev: &Self,
|
||||||
|
view_state: &mut Self::ViewState,
|
||||||
|
ctx: &mut ViewCtx,
|
||||||
|
element: Mut<'el, Self::Element>,
|
||||||
|
) -> Mut<'el, Self::Element> {
|
||||||
|
let element = self
|
||||||
|
.element
|
||||||
|
.rebuild(&prev.element, view_state, ctx, element);
|
||||||
|
element.node.apply_props(element.props);
|
||||||
|
(self.callback)(element.node);
|
||||||
|
element
|
||||||
|
}
|
||||||
|
|
||||||
|
fn teardown(
|
||||||
|
&self,
|
||||||
|
view_state: &mut Self::ViewState,
|
||||||
|
ctx: &mut ViewCtx,
|
||||||
|
el: Mut<'_, Self::Element>,
|
||||||
|
) {
|
||||||
|
self.element.teardown(view_state, ctx, el);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn message(
|
||||||
|
&self,
|
||||||
|
view_state: &mut Self::ViewState,
|
||||||
|
id_path: &[ViewId],
|
||||||
|
message: DynMessage,
|
||||||
|
app_state: &mut State,
|
||||||
|
) -> MessageResult<Action, DynMessage> {
|
||||||
|
self.element
|
||||||
|
.message(view_state, id_path, message, app_state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State, Action, V, F> View<State, Action, ViewCtx, DynMessage>
|
||||||
|
for BeforeTeardown<State, Action, V, F>
|
||||||
|
where
|
||||||
|
State: 'static,
|
||||||
|
Action: 'static,
|
||||||
|
F: Fn(&V::DomNode) + 'static,
|
||||||
|
V: DomView<State, Action> + 'static,
|
||||||
|
{
|
||||||
|
type Element = V::Element;
|
||||||
|
|
||||||
|
type ViewState = V::ViewState;
|
||||||
|
|
||||||
|
fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
|
||||||
|
self.element.build(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild<'el>(
|
||||||
|
&self,
|
||||||
|
prev: &Self,
|
||||||
|
view_state: &mut Self::ViewState,
|
||||||
|
ctx: &mut ViewCtx,
|
||||||
|
element: Mut<'el, Self::Element>,
|
||||||
|
) -> Mut<'el, Self::Element> {
|
||||||
|
self.element
|
||||||
|
.rebuild(&prev.element, view_state, ctx, element)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn teardown(
|
||||||
|
&self,
|
||||||
|
view_state: &mut Self::ViewState,
|
||||||
|
ctx: &mut ViewCtx,
|
||||||
|
el: Mut<'_, Self::Element>,
|
||||||
|
) {
|
||||||
|
(self.callback)(el.node);
|
||||||
|
self.element.teardown(view_state, ctx, el);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn message(
|
||||||
|
&self,
|
||||||
|
view_state: &mut Self::ViewState,
|
||||||
|
id_path: &[ViewId],
|
||||||
|
message: DynMessage,
|
||||||
|
app_state: &mut State,
|
||||||
|
) -> MessageResult<Action, DynMessage> {
|
||||||
|
self.element
|
||||||
|
.message(view_state, id_path, message, app_state)
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ pub const SVG_NS: &str = "http://www.w3.org/2000/svg";
|
||||||
/// The MathML namespace
|
/// The MathML namespace
|
||||||
pub const MATHML_NS: &str = "http://www.w3.org/1998/Math/MathML";
|
pub const MATHML_NS: &str = "http://www.w3.org/1998/Math/MathML";
|
||||||
|
|
||||||
|
mod after_update;
|
||||||
mod app;
|
mod app;
|
||||||
mod attribute;
|
mod attribute;
|
||||||
mod attribute_value;
|
mod attribute_value;
|
||||||
|
@ -48,6 +49,9 @@ pub mod elements;
|
||||||
pub mod interfaces;
|
pub mod interfaces;
|
||||||
pub mod svg;
|
pub mod svg;
|
||||||
|
|
||||||
|
pub use after_update::{
|
||||||
|
after_build, after_rebuild, before_teardown, AfterBuild, AfterRebuild, BeforeTeardown,
|
||||||
|
};
|
||||||
pub use app::App;
|
pub use app::App;
|
||||||
pub use attribute::{Attr, Attributes, ElementWithAttributes, WithAttributes};
|
pub use attribute::{Attr, Attributes, ElementWithAttributes, WithAttributes};
|
||||||
pub use attribute_value::{AttributeValue, IntoAttributeValue};
|
pub use attribute_value::{AttributeValue, IntoAttributeValue};
|
||||||
|
@ -137,6 +141,39 @@ pub trait DomView<State, Action = ()>:
|
||||||
core::adapt(self, f)
|
core::adapt(self, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// See [`after_build`](`after_update::after_build`)
|
||||||
|
fn after_build<F>(self, callback: F) -> AfterBuild<State, Action, Self, F>
|
||||||
|
where
|
||||||
|
State: 'static,
|
||||||
|
Action: 'static,
|
||||||
|
Self: Sized,
|
||||||
|
F: Fn(&Self::DomNode) + 'static,
|
||||||
|
{
|
||||||
|
after_build(self, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See [`after_rebuild`](`after_update::after_rebuild`)
|
||||||
|
fn after_rebuild<F>(self, callback: F) -> AfterRebuild<State, Action, Self, F>
|
||||||
|
where
|
||||||
|
State: 'static,
|
||||||
|
Action: 'static,
|
||||||
|
Self: Sized,
|
||||||
|
F: Fn(&Self::DomNode) + 'static,
|
||||||
|
{
|
||||||
|
after_rebuild(self, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See [`before_teardown`](`after_update::before_teardown`)
|
||||||
|
fn before_teardown<F>(self, callback: F) -> BeforeTeardown<State, Action, Self, F>
|
||||||
|
where
|
||||||
|
State: 'static,
|
||||||
|
Action: 'static,
|
||||||
|
Self: Sized,
|
||||||
|
F: Fn(&Self::DomNode) + 'static,
|
||||||
|
{
|
||||||
|
before_teardown(self, callback)
|
||||||
|
}
|
||||||
|
|
||||||
/// See [`map_state`](`core::map_state`)
|
/// See [`map_state`](`core::map_state`)
|
||||||
fn map_state<ParentState, F>(self, f: F) -> MapState<ParentState, State, Self, F>
|
fn map_state<ParentState, F>(self, f: F) -> MapState<ParentState, State, Self, F>
|
||||||
where
|
where
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "raw_dom_access"
|
||||||
|
version = "0.1.0"
|
||||||
|
publish = false
|
||||||
|
license.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
console_error_panic_hook = "0.1"
|
||||||
|
console_log = "1.0.0"
|
||||||
|
log = "0.4.22"
|
||||||
|
web-sys = "0.3.69"
|
||||||
|
xilem_web = { path = "../.." }
|
|
@ -0,0 +1,8 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>xilem web | raw DOM access example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,64 @@
|
||||||
|
// Copyright 2023 the Xilem Authors
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
//! This example demonstrates a dirty hack that should be avoided,
|
||||||
|
//! and only used with extreme caution in cases where direct access
|
||||||
|
//! to the raw DOM nodes is necessary
|
||||||
|
//! (e.g. when using external JS libraries).
|
||||||
|
//!
|
||||||
|
//! Please also note that no rebuild is triggered
|
||||||
|
//! after a callback has been performed in
|
||||||
|
//! `after_build`, `after_rebuild` or `before_teardown`.
|
||||||
|
|
||||||
|
use std::{cell::Cell, rc::Rc};
|
||||||
|
|
||||||
|
use xilem_web::{
|
||||||
|
core::one_of::Either, document_body, elements::html, interfaces::Element, App, DomView,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct AppState {
|
||||||
|
focus: Rc<Cell<bool>>,
|
||||||
|
show_input: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app_logic(app_state: &mut AppState) -> impl Element<AppState> {
|
||||||
|
html::div(if app_state.show_input {
|
||||||
|
let focus = Rc::clone(&app_state.focus);
|
||||||
|
Either::A(html::div((
|
||||||
|
html::button("remove input").on_click(|app_state: &mut AppState, _| {
|
||||||
|
app_state.show_input = false;
|
||||||
|
}),
|
||||||
|
html::input(())
|
||||||
|
.after_build(|_| {
|
||||||
|
log::debug!("element was build");
|
||||||
|
})
|
||||||
|
.after_rebuild(move |el| {
|
||||||
|
log::debug!("element was re-build");
|
||||||
|
if focus.get() {
|
||||||
|
let _ = el.focus();
|
||||||
|
// Reset `focus` to avoid calling `el.focus` on every rebuild.
|
||||||
|
focus.set(false); // NOTE: this does NOT trigger a rebuild.
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.before_teardown(|_| {
|
||||||
|
log::debug!("element will be removed");
|
||||||
|
}),
|
||||||
|
html::button("Focus the input").on_click(|app_state: &mut AppState, _| {
|
||||||
|
app_state.focus.set(true);
|
||||||
|
}),
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Either::B(
|
||||||
|
html::button("show input").on_click(|app_state: &mut AppState, _| {
|
||||||
|
app_state.show_input = true;
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
_ = console_log::init_with_level(log::Level::Debug);
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
App::new(document_body(), AppState::default(), app_logic).run();
|
||||||
|
}
|
Loading…
Reference in New Issue