From 199594a8f5ba100f41ba333539ba69f45822208f Mon Sep 17 00:00:00 2001 From: Michal Pokrywka Date: Thu, 1 Oct 2026 20:22:18 +0200 Subject: [PATCH] ResourceTable holds its row state --- CHANGES.md | 7 +- src/resource_table/mod.rs | 17 +++-- src/resource_table/row_form.rs | 115 +++++++++++++++++++++++++++++---- 3 files changed, 123 insertions(+), 16 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c894dfe..760c620 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,11 +5,16 @@ ### Added -* `with_stable_loader` and `WithStableLoader` - a loader that renders once, when the resource first becomes ready, and keeps the rendered content on later `Loading`/`Error`, so edit forms survive background re-fetches +* `with_stable_loader` and `WithStableLoader` - a loader that renders once, when the resource first becomes ready, + and keeps the rendered content on later `Loading`/`Error`, so edit forms survive background re-fetches ### Fixed * Set `type="button"` for buttons inside forms which are not submits +* `ResourceTable`: the error of a failed delete is shown below the row +* `ResourceTable`: a row keeps its state (edit form with what was typed in, error message) when it is rendered anew + because its item changed or came back, f. ex. after an optimistic update or removal is rolled back +* `ResourceTable`: "Cancel" drops the changes made in the form, the next edit starts from the item ## 0.2.1 - 2026-09-07 diff --git a/src/resource_table/mod.rs b/src/resource_table/mod.rs index 41a8fd3..6f25c14 100644 --- a/src/resource_table/mod.rs +++ b/src/resource_table/mod.rs @@ -11,6 +11,7 @@ pub use row_form::{ AsyncResult, CancelCallback, CreateFn, ProcessCallback, ResourceTableLabels, RowResult, RowState, row_form, }; +use row_form::{RowStore, row_form_with_store}; type ComputedListItem = Computed>; type ComputedList = Computed>>>>; @@ -36,6 +37,11 @@ type ComputedList = Computed>>>>; /// // ... /// } /// ``` +/// +/// ## Row state +/// +/// The state of a row (view or edit form, what was typed in, error message) is kept per +/// position in `list`. #[derive(Clone)] pub struct ResourceTable { pub list: ComputedList, @@ -72,12 +78,15 @@ impl Resource::Ready(list) => { for item in &*list { let props = props.clone(); + // Outlives the row, which is rendered anew whenever its item changes + // or comes back (f. ex. an optimistic update rolled back after a failure) + let store = RowStore::new(RowState::View { + confirm_delete: false, + }); let row = item.render_value_option(move |item| { item.map(|item| { - row_form( - RowState::View { - confirm_delete: false, - }, + row_form_with_store( + store.clone(), &item, props.create_form_model, props.update_model, diff --git a/src/resource_table/row_form.rs b/src/resource_table/row_form.rs index 0b1f847..e908cf0 100644 --- a/src/resource_table/row_form.rs +++ b/src/resource_table/row_form.rs @@ -1,4 +1,4 @@ -use std::{future::Future, pin::Pin, rc::Rc}; +use std::{cell::RefCell, future::Future, pin::Pin, rc::Rc}; use vertigo::{Computed, DomComment, DomNode, Value, bind_rc, bind_spawn, css, dom, transaction}; use crate::button::{Button, ButtonColor, ButtonVariant}; @@ -60,10 +60,78 @@ pub fn row_form( labels: ResourceTableLabels, ) -> DomNode { - let state = Value::new(initial_state); - let error = Value::new(None::); - let form_model = create_form_model(item); - let optimistic_item: Value> = Value::new(None); + row_form_with_store( + RowStore::new(initial_state), + item, + create_form_model, + update_model, + render_view, + render_form, + process_label, + process, + cancel_label, + cancel, + delete, + labels, + ) +} + +/// State of a row kept outside of its rendering. +/// +/// A row is rendered anew whenever its item changes or comes back: an optimistic update or +/// removal, its rollback after a failure, a background refresh. [`ResourceTable`](super::ResourceTable) +/// keeps one store per list position, so the row stays in the edit form with what was typed +/// in, and the message about a failed save or delete is shown in the row that comes back. +#[derive(Clone)] +pub(crate) struct RowStore { + state: Value, + error: Value>, + /// Created from the item when editing starts, so that the next edit doesn't start from + /// changes dropped with "Cancel". A failed save returns to the same form. + form_model: Rc>>, + optimistic_item: Value>, +} + +impl RowStore { + pub(crate) fn new(initial_state: RowState) -> Self { + Self { + state: Value::new(initial_state), + error: Value::new(None), + form_model: Rc::new(RefCell::new(None)), + optimistic_item: Value::new(None), + } + } +} + +/// [`row_form`] with its state in `store`, owned by the caller. +#[allow(clippy::too_many_arguments)] +pub(crate) fn row_form_with_store< + Model: Clone + PartialEq + 'static, + FormModel: Clone + 'static, +>( + store: RowStore, + item: &Model, + create_form_model: fn(&Model) -> FormModel, + update_model: fn(&Model, &FormModel, &vertigo::Context) -> RowResult, + render_view: fn(&Model, create_buttons: CreateFn, alert: Computed) -> DomNode, + render_form: fn(&FormModel, buttons: DomNode) -> DomNode, + + process_label: String, + process: ProcessCallback, + + cancel_label: String, + cancel: CancelCallback, + + delete: Option>, + + labels: ResourceTableLabels, +) -> DomNode { + let RowStore { + state, + error, + form_model, + optimistic_item, + } = store; let alert_view = state.map(|value| { if let RowState::View { confirm_delete } = value @@ -113,13 +181,17 @@ pub fn row_form( }; let state_for_buttons = state.clone(); + let error_for_buttons = error.clone(); + let form_model_for_buttons = form_model.clone(); + let item_for_buttons = item.clone(); let delete_for_buttons = delete.clone(); let edit_label = labels.edit.clone(); let delete_label = labels.delete.clone(); let create_buttons = move || { let delete_view_inner = if delete_for_buttons.is_some() { - let delete_click = bind_rc!(state_for_buttons, || { + let delete_click = bind_rc!(state_for_buttons, error_for_buttons, || { + error_for_buttons.set(None); state_for_buttons.set(RowState::View { confirm_delete: true }); }); @@ -135,11 +207,24 @@ pub fn row_form( dom! {
} }; + let edit_click = bind_rc!( + state_for_buttons, + error_for_buttons, + form_model_for_buttons, + item_for_buttons, + || { + *form_model_for_buttons.borrow_mut() = + Some(create_form_model(&item_for_buttons)); + error_for_buttons.set(None); + state_for_buttons.set(RowState::Edit); + } + ); + dom! {
}; - let rendered_form = render_form(&form_model, buttons); + let rendered_form = render_form(&form, buttons); dom! {