Description

This example demonstrates how to build a summary or review step that:

  1. Tracks data updates across the wizard using onDataUpdate.
  2. Avoids granular reactivity issues by repainting the entire form on demand when selecting/entering the tab via onStepEnter and callback(buildForm(currentData)).
  3. Recursively parses objects, arrays, and primitive values into a collapsible VanJS tree structure using native <details> and <summary> elements.
  4. Highlights different data types (strings, numbers, booleans, objects, arrays, and null/undefined) with distinct typography and colors.
  5. Keeps the step valid using setModelData(true).

Code

// 1. Import only the VanJS tags used below
const { div, span, strong, p, details, summary, ul, li } = van.tags;

// 2. Reusable inline styles
const styles = {
    container: "padding: 1rem; font-family: system-ui, -apple-system, sans-serif; color: #1e293b; max-width: 720px;",
    header: "margin-bottom: 1.25rem;",
    title: "font-size: 1.1rem; font-weight: 600; margin: 0 0 0.25rem 0; color: #0f172a;",
    subtitle: "color: #64748b; font-size: 0.85rem; margin: 0;",
    treeBox: "background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 1rem; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.85rem; overflow-x: auto;",
    details: "margin-left: 0.5rem; margin-top: 0.25rem;",
    summary: "cursor: pointer; user-select: none; font-weight: 600; color: #2563eb; padding: 0.15rem 0;",
    badge: "font-size: 0.75rem; color: #64748b; font-weight: normal; margin-left: 0.35rem;",
    list: "list-style: none; margin: 0.2rem 0 0.2rem 0.5rem; padding-left: 0.75rem; border-left: 1px dashed #cbd5e1;",
    item: "margin: 0.25rem 0;",
    key: "color: #0f172a; font-weight: 600;",
    strVal: "color: #166534;",
    numVal: "color: #2563eb;",
    boolVal: "color: #7c3aed;",
    nullVal: "color: #94a3b8; font-style: italic;",
    emptyText: "color: #94a3b8; font-style: italic; margin: 0;"
};

// 3. Latest data accumulated by the wizard
let currentData = entityData || {};

// 4. Mark the step as valid, since this is a read-only summary view
setModelData(true);

// 5. Listen for and store the updates coming from the other wizard steps
onDataUpdate((updatedData) => {
    currentData = updatedData || {};
});

// 6. Helpers to format primitive values
function renderPrimitive(val) {
    if (val === null) {
        return span({ style: styles.nullVal }, "null");
    }
    if (val === undefined) {
        return span({ style: styles.nullVal }, "undefined");
    }
    if (typeof val === "string") {
        return span({ style: styles.strVal }, `"${val}"`);
    }
    if (typeof val === "number") {
        return span({ style: styles.numVal }, String(val));
    }
    if (typeof val === "boolean") {
        return span({ style: styles.boolVal }, String(val));
    }
    return span(String(val));
}

// 7. Static recursive rendering as a tree
function renderTree(data) {
    if (!data || typeof data !== "object" || Object.keys(data).length === 0) {
        return p({ style: styles.emptyText }, "No data has been entered yet.");
    }

    const keys = Object.keys(data);
    const childItems = keys.map((k) => {
        const itemVal = data[k];

        if (itemVal !== null && typeof itemVal === "object") {
            const isArray = Array.isArray(itemVal);
            const count = Object.keys(itemVal).length;
            const badgeLabel = isArray ? `[${count} items]` : `{${count} fields}`;

            return li({ style: styles.item },
                details({ style: styles.details, open: true },
                    summary({ style: styles.summary },
                        span({ style: styles.key }, k),
                        span({ style: styles.badge }, badgeLabel)
                    ),
                    renderTree(itemVal)
                )
            );
        }

        return li({ style: styles.item },
            span({ style: styles.key }, `${k}: `),
            renderPrimitive(itemVal)
        );
    });

    return ul({ style: styles.list }, ...childItems);
}

// 8. Build the component with the current data
function buildForm(data) {
    return div({ style: styles.container },
        div({ style: styles.header },
            p({ style: styles.title },
                strong("Wizard Data Tree")
            ),
            p({ style: styles.subtitle },
                "Hierarchical structure of every parameter entered in the wizard so far."
            )
        ),

        div({ style: styles.treeBox },
            renderTree(data)
        )
    );
}

// 9. Repaint the form only when the tab is entered or selected
onStepEnter(() => {
    setModelData(true);
    callback(buildForm(currentData));
});

// 10. Initial paint
callback(buildForm(currentData));