tablo-core 0.4.0

The core toolkit types for Tablo, a server-rendered admin toolkit on Topcoat and Toasty.
Documentation
//! The table's write form and the dialog that confirms its destructive writes.

use tablo_ui::{
    ButtonSize, ButtonVariant, alert_dialog, button, dialog_content, dialog_description,
    dialog_footer, dialog_header, dialog_title,
};
use topcoat::{
    context::Cx,
    runtime::{Event, expr},
    view::*,
};

use super::{Frame, table_dom_id};
use crate::table::state::{ConfirmSignals, TableSignals, TableState};

impl Frame<'_> {
    /// Whether the table posts any write: a delete or a custom action.
    pub(super) fn writes(&self) -> bool {
        self.delete_prefix.is_some() || self.actions_prefix.is_some()
    }

    /// Render the table's one write form inside the dialog that confirms it.
    ///
    /// Every write control names this form: a direct write submits it to its own `formaction`,
    /// and a destructive one opens the dialog as a modal on its POST target, whose submit carries
    /// the `confirm=1` marker the handlers require. Cancel and Escape close the dialog and clear
    /// the target. The form carries the bulk selection, which the row routes ignore.
    pub(super) fn render_write_form<'a>(
        &self,
        cx: &'a Cx,
        state: &TableState,
        signals: &TableSignals,
    ) -> Option<BoxView<'a>> {
        if !self.writes() {
            return None;
        }
        let id = table_dom_id(state, "confirm");
        let form_id = table_dom_id(state, "writes");
        let title_id = format!("{id}-title");
        let description_id = format!("{id}-description");
        let csrf = crate::csrf::current_token(cx);
        let ConfirmSignals {
            action,
            title,
            label,
            bulk: counts,
        } = signals.confirm.clone();
        let selection = signals.bulk.clone();
        let count = expr!({
            let wire = selection.get();
            raw!(
                "cx.hydrate(String(String(${wire}).split(',').filter(Boolean).length))",
                wire.split(',')
                    .filter(|key| !key.is_empty())
                    .count()
                    .to_string()
            )
        });
        Some(
            view! {
                cx =>
                alert_dialog(
                    open: false,
                    attrs: attributes! {
                        id=(id)
                        aria-labelledby=(title_id.clone())
                        aria-describedby=(description_id.clone())
                        @close=$(|_e: Event| action.set("".to_owned()))
                    },
                    <form
                        id=(form_id)
                        class="contents"
                        method="post"
                        :action=$(action.get())
                    >
                        (crate::csrf::field(cx, &csrf))
                        <input type="hidden" name="confirm" value="1">
                        <input type="hidden" name="ids" :value=$(selection.get())>
                        dialog_content(
                            dialog_header(
                                dialog_title(
                                    attrs: attributes! { id=(title_id) },
                                    $(title.get())
                                )
                                dialog_description(
                                    attrs: attributes! { id=(description_id) },
                                    "This action cannot be undone."
                                    <span :hidden=$(!counts.get())>
                                        " Selected records: "
                                        (count)
                                    </span>
                                )
                            )
                            dialog_footer(
                                button(
                                    variant: ButtonVariant::Outline,
                                    size: ButtonSize::Md,
                                    attrs: attributes! { type="submit" formmethod="dialog" },
                                    "Cancel"
                                )
                                button(
                                    variant: ButtonVariant::Destructive,
                                    size: ButtonSize::Md,
                                    attrs: attributes! { type="submit" },
                                    $(label.get())
                                )
                            )
                        )
                    </form>
                )
            }
            .boxed(),
        )
    }
}

/// A write control's attributes: a direct write submits the table's write `form` to `action`, and a
/// destructive one opens the confirmation dialog around the form on it, titled `title` and
/// confirmed by `label`. Both carry `action` as `formaction`; on the dialog's trigger, which
/// submits nothing itself, it only names the write.
/// `bulk` says whether the write takes the selection, which the dialog then counts.
pub(super) fn write_trigger(
    cx: &Cx,
    form: &str,
    signals: &TableSignals,
    action: String,
    confirm: Option<(&'static str, &'static str)>,
    bulk: bool,
) -> Attributes {
    let Some((title, label)) = confirm else {
        return attributes! {
            cx =>
            type="submit"
            form=(form.to_string())
            formaction=(action)
        };
    };
    let ConfirmSignals {
        action: target,
        title: shown_title,
        label: shown_label,
        bulk: counts,
    } = signals.confirm.clone();
    let (title, label, form) = (title.to_owned(), label.to_owned(), form.to_owned());
    attributes! {
        cx =>
        type="button"
        formaction=(action.clone())
        @click=$(|_e: Event| {
            shown_title.set(title.clone());
            shown_label.set(label.clone());
            counts.set(bulk);
            target.set(action.clone());
            raw!(
                "document.getElementById(String(${form})).closest('dialog').showModal()",
                (),
            );
        })
    }
}