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"
|
||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
|
||||
[[package]]
|
||||
name = "raw_dom_access"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
"log",
|
||||
"web-sys",
|
||||
"xilem_web",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "read-fonts"
|
||||
version = "0.19.3"
|
||||
|
|
|
@ -12,6 +12,7 @@ members = [
|
|||
"xilem_web/web_examples/fetch",
|
||||
"xilem_web/web_examples/todomvc",
|
||||
"xilem_web/web_examples/mathml_svg",
|
||||
"xilem_web/web_examples/raw_dom_access",
|
||||
"xilem_web/web_examples/spawn_tasks",
|
||||
"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
|
||||
pub const MATHML_NS: &str = "http://www.w3.org/1998/Math/MathML";
|
||||
|
||||
mod after_update;
|
||||
mod app;
|
||||
mod attribute;
|
||||
mod attribute_value;
|
||||
|
@ -48,6 +49,9 @@ pub mod elements;
|
|||
pub mod interfaces;
|
||||
pub mod svg;
|
||||
|
||||
pub use after_update::{
|
||||
after_build, after_rebuild, before_teardown, AfterBuild, AfterRebuild, BeforeTeardown,
|
||||
};
|
||||
pub use app::App;
|
||||
pub use attribute::{Attr, Attributes, ElementWithAttributes, WithAttributes};
|
||||
pub use attribute_value::{AttributeValue, IntoAttributeValue};
|
||||
|
@ -137,6 +141,39 @@ pub trait DomView<State, Action = ()>:
|
|||
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`)
|
||||
fn map_state<ParentState, F>(self, f: F) -> MapState<ParentState, State, Self, F>
|
||||
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