mirror of https://github.com/linebender/xilem
xilem_web: Allow `DomFragment` instead of `DomView` as `app_logic` (#482)
Should fix #461. This allows a `ViewSequence` (called `DomFragment`) of `DomView`s as root component. The `counter` example is updated to show this new behavior.
This commit is contained in:
parent
e27b3ce0c2
commit
bb13f1a760
|
@ -1,11 +1,12 @@
|
|||
// Copyright 2023 the Xilem Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{DomNode, ViewCtx};
|
||||
use crate::{elements::DomChildrenSplice, AnyPod, DomFragment, ViewCtx};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use crate::{DomView, DynMessage, PodMut};
|
||||
use xilem_core::{MessageResult, ViewId};
|
||||
use crate::DynMessage;
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
use xilem_core::{AppendVec, MessageResult, ViewId};
|
||||
|
||||
pub(crate) struct AppMessage {
|
||||
pub id_path: Rc<[ViewId]>,
|
||||
|
@ -13,15 +14,19 @@ pub(crate) struct AppMessage {
|
|||
}
|
||||
|
||||
/// The type responsible for running your app.
|
||||
pub struct App<T, V: DomView<T>, F: FnMut(&mut T) -> V>(Rc<RefCell<AppInner<T, V, F>>>);
|
||||
pub struct App<State, Fragment: DomFragment<State>, InitFragment>(
|
||||
Rc<RefCell<AppInner<State, Fragment, InitFragment>>>,
|
||||
);
|
||||
|
||||
struct AppInner<T, V: DomView<T>, F: FnMut(&mut T) -> V> {
|
||||
data: T,
|
||||
struct AppInner<State, Fragment: DomFragment<State>, InitFragment> {
|
||||
data: State,
|
||||
root: web_sys::Node,
|
||||
app_logic: F,
|
||||
view: Option<V>,
|
||||
state: Option<V::ViewState>,
|
||||
element: Option<V::Element>,
|
||||
app_logic: InitFragment,
|
||||
fragment: Option<Fragment>,
|
||||
fragment_state: Option<Fragment::SeqState>,
|
||||
fragment_append_scratch: AppendVec<AnyPod>,
|
||||
vec_splice_scratch: Vec<AnyPod>,
|
||||
elements: Vec<AnyPod>,
|
||||
cx: ViewCtx,
|
||||
}
|
||||
|
||||
|
@ -31,15 +36,22 @@ pub(crate) trait AppRunner {
|
|||
fn clone_box(&self) -> Box<dyn AppRunner>;
|
||||
}
|
||||
|
||||
impl<T: 'static, V: DomView<T> + 'static, F: FnMut(&mut T) -> V + 'static> Clone for App<T, V, F> {
|
||||
impl<State, Fragment: DomFragment<State>, InitFragment> Clone
|
||||
for App<State, Fragment, InitFragment>
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
App(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static, V: DomView<T> + 'static, F: FnMut(&mut T) -> V + 'static> App<T, V, F> {
|
||||
impl<State, Fragment, InitFragment> App<State, Fragment, InitFragment>
|
||||
where
|
||||
State: 'static,
|
||||
Fragment: DomFragment<State> + 'static,
|
||||
InitFragment: FnMut(&mut State) -> Fragment + 'static,
|
||||
{
|
||||
/// Create an instance of your app with the given logic and initial state.
|
||||
pub fn new(root: impl AsRef<web_sys::Node>, data: T, app_logic: F) -> Self {
|
||||
pub fn new(root: impl AsRef<web_sys::Node>, data: State, app_logic: InitFragment) -> Self {
|
||||
let inner = AppInner::new(root.as_ref().clone(), data, app_logic);
|
||||
let app = App(Rc::new(RefCell::new(inner)));
|
||||
app.0.borrow_mut().cx.set_runner(app.clone());
|
||||
|
@ -57,69 +69,85 @@ impl<T: 'static, V: DomView<T> + 'static, F: FnMut(&mut T) -> V + 'static> App<T
|
|||
}
|
||||
}
|
||||
|
||||
impl<T, V: DomView<T>, F: FnMut(&mut T) -> V> AppInner<T, V, F> {
|
||||
pub fn new(root: web_sys::Node, data: T, app_logic: F) -> Self {
|
||||
impl<State, Fragment: DomFragment<State>, InitFragment: FnMut(&mut State) -> Fragment>
|
||||
AppInner<State, Fragment, InitFragment>
|
||||
{
|
||||
pub fn new(root: web_sys::Node, data: State, app_logic: InitFragment) -> Self {
|
||||
let cx = ViewCtx::default();
|
||||
AppInner {
|
||||
data,
|
||||
root,
|
||||
app_logic,
|
||||
view: None,
|
||||
state: None,
|
||||
element: None,
|
||||
fragment: None,
|
||||
fragment_state: None,
|
||||
elements: Vec::new(),
|
||||
cx,
|
||||
fragment_append_scratch: Default::default(),
|
||||
vec_splice_scratch: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_app(&mut self) {
|
||||
if self.view.is_none() {
|
||||
let view = (self.app_logic)(&mut self.data);
|
||||
let (mut element, state) = view.build(&mut self.cx);
|
||||
element.node.apply_props(&mut element.props);
|
||||
self.view = Some(view);
|
||||
self.state = Some(state);
|
||||
if self.fragment.is_none() {
|
||||
let fragment = (self.app_logic)(&mut self.data);
|
||||
let state = fragment.seq_build(&mut self.cx, &mut self.fragment_append_scratch);
|
||||
self.fragment = Some(fragment);
|
||||
self.fragment_state = Some(state);
|
||||
|
||||
// TODO should the element provide a separate method to access reference instead?
|
||||
let node: &web_sys::Node = element.node.as_ref();
|
||||
self.root.append_child(node).unwrap();
|
||||
self.element = Some(element);
|
||||
let append_vec = std::mem::take(&mut self.fragment_append_scratch);
|
||||
|
||||
self.elements = append_vec.into_inner();
|
||||
for pod in &self.elements {
|
||||
self.root.append_child(pod.node.as_ref()).unwrap_throw();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static, V: DomView<T> + 'static, F: FnMut(&mut T) -> V + 'static> AppRunner
|
||||
for App<T, V, F>
|
||||
impl<State, Fragment, InitFragment> AppRunner for App<State, Fragment, InitFragment>
|
||||
where
|
||||
State: 'static,
|
||||
Fragment: DomFragment<State> + 'static,
|
||||
InitFragment: FnMut(&mut State) -> Fragment + 'static,
|
||||
{
|
||||
// For now we handle the message synchronously, but it would also
|
||||
// make sense to to batch them (for example with requestAnimFrame).
|
||||
fn handle_message(&self, message: AppMessage) {
|
||||
let mut inner_guard = self.0.borrow_mut();
|
||||
let inner = &mut *inner_guard;
|
||||
if let Some(view) = &mut inner.view {
|
||||
let message_result = view.message(
|
||||
inner.state.as_mut().unwrap(),
|
||||
if let Some(fragment) = &mut inner.fragment {
|
||||
let message_result = fragment.seq_message(
|
||||
inner.fragment_state.as_mut().unwrap(),
|
||||
&message.id_path,
|
||||
message.body,
|
||||
&mut inner.data,
|
||||
);
|
||||
|
||||
// Each of those results are currently resulting in a rebuild, that may be subject to change
|
||||
match message_result {
|
||||
MessageResult::Nop | MessageResult::Action(_) => {
|
||||
// Nothing to do.
|
||||
}
|
||||
MessageResult::RequestRebuild => {
|
||||
// TODO force a rebuild?
|
||||
}
|
||||
MessageResult::RequestRebuild | MessageResult::Nop | MessageResult::Action(_) => {}
|
||||
MessageResult::Stale(_) => {
|
||||
// TODO perhaps inform the user that a stale request bubbled to the top?
|
||||
}
|
||||
}
|
||||
|
||||
let new_view = (inner.app_logic)(&mut inner.data);
|
||||
let el = inner.element.as_mut().unwrap();
|
||||
let pod_mut = PodMut::new(&mut el.node, &mut el.props, &inner.root, false);
|
||||
new_view.rebuild(view, inner.state.as_mut().unwrap(), &mut inner.cx, pod_mut);
|
||||
*view = new_view;
|
||||
let new_fragment = (inner.app_logic)(&mut inner.data);
|
||||
let mut dom_children_splice = DomChildrenSplice::new(
|
||||
&mut inner.fragment_append_scratch,
|
||||
&mut inner.elements,
|
||||
&mut inner.vec_splice_scratch,
|
||||
&inner.root,
|
||||
inner.cx.fragment.clone(),
|
||||
false,
|
||||
);
|
||||
new_fragment.seq_rebuild(
|
||||
fragment,
|
||||
inner.fragment_state.as_mut().unwrap(),
|
||||
&mut inner.cx,
|
||||
&mut dom_children_splice,
|
||||
);
|
||||
*fragment = new_fragment;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
use xilem_web::{
|
||||
document_body,
|
||||
elements::html as el,
|
||||
interfaces::{Element, HtmlButtonElement, HtmlDivElement},
|
||||
interfaces::{Element, HtmlButtonElement},
|
||||
App, DomFragment,
|
||||
};
|
||||
|
||||
|
@ -55,10 +55,10 @@ fn huzzah(state: &mut AppState) -> impl DomFragment<AppState> {
|
|||
(state.clicks >= 5).then_some("Huzzah, clicked at least 5 times")
|
||||
}
|
||||
|
||||
fn app_logic(state: &mut AppState) -> impl HtmlDivElement<AppState> {
|
||||
el::div((
|
||||
/// Even the root `app_logic` can return a sequence of views
|
||||
fn app_logic(state: &mut AppState) -> impl DomFragment<AppState> {
|
||||
(
|
||||
el::span(format!("clicked {} times", state.clicks)).class(state.class),
|
||||
huzzah(state),
|
||||
el::br(()),
|
||||
btn("+1 click", |state, _| state.increment()),
|
||||
btn("-1 click", |state, _| state.decrement()),
|
||||
|
@ -66,8 +66,10 @@ fn app_logic(state: &mut AppState) -> impl HtmlDivElement<AppState> {
|
|||
btn("a different class", |state, _| state.change_class()),
|
||||
btn("change text", |state, _| state.change_text()),
|
||||
el::br(()),
|
||||
huzzah(state),
|
||||
el::br(()),
|
||||
state.text.clone(),
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
|
|
Loading…
Reference in New Issue