Example 5
Description
This example demonstrates how to build a summary or review step that:
- Tracks data updates across the wizard using
onDataUpdate. - Avoids granular reactivity issues by repainting the entire form on demand when selecting/entering the tab via
onStepEnterandcallback(buildForm(currentData)). - Recursively parses objects, arrays, and primitive values into a collapsible VanJS tree structure using native
<details>and<summary>elements. - Highlights different data types (strings, numbers, booleans, objects, arrays, and null/undefined) with distinct typography and colors.
- 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));