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:
Markus Kohlhase 2024-08-09 00:44:49 +02:00 committed by GitHub
parent 2eda5a2a50
commit 5b6ebc324f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 388 additions and 0 deletions

11
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

@ -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 = "../.." }

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<title>xilem web | raw DOM access example</title>
</head>
<body>
</body>
</html>

View File

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