Example 4
Description
This example demonstrates how to:
- Fetch data from an external API (randomuser.me) using the platform’s
httputility (Nuxt 4useFetchinstance). - Render an interactive data
<table>with VanJS displaying user details (photo, name, email, location) with clickable, highlightable rows. - Automatically refresh users whenever the tab is entered or refreshed using the
onStepEnterhook. - Paint the VanJS form exclusively when refreshing/loading users by calling
callback(formElement)within the refresh routine and never before. - Pass the selected user’s full name to subsequent wizard tabs using
setModelData(true, { userName: ... }). - Invalidate the step (
setModelData(false)) until a user is explicitly selected.
Code
// 1. Import only the VanJS tags used below
const { div, span, table, thead, tbody, tr, th, td, img, p } = van.tags;
// 2. Reusable inline styles
const styles = {
container: "padding: 1rem; font-family: system-ui, -apple-system, sans-serif; color: #1e293b; max-width: 680px;",
header: "margin-bottom: 1rem;",
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;",
table: "width: 100%; border-collapse: collapse; font-size: 0.85rem; margin-top: 0.75rem; border: 1px solid #e2e8f0; border-radius: 6px; overflow: hidden;",
th: "background-color: #f1f5f9; color: #475569; font-weight: 600; text-align: left; padding: 0.6rem 0.75rem; border-bottom: 1px solid #cbd5e1;",
row: "cursor: pointer; transition: background-color 0.15s ease; border-bottom: 1px solid #f1f5f9; background-color: white;",
selectedRow: "cursor: pointer; transition: background-color 0.15s ease; border-bottom: 1px solid #93c5fd; background-color: #eff6ff;",
td: "padding: 0.5rem 0.75rem; vertical-align: middle;",
tableAvatar: "width: 32px; height: 32px; border-radius: 50%; object-fit: cover; display: block;",
userCard: "margin-top: 1.25rem; padding: 1rem; border: 1px solid #bfdbfe; border-radius: 8px; background-color: #f0f9ff; display: flex; gap: 1rem; align-items: center;",
cardAvatar: "width: 52px; height: 52px; border-radius: 50%; border: 2px solid #3b82f6; object-fit: cover;",
userInfo: "flex: 1; min-width: 0;",
userName: "font-weight: 600; font-size: 0.95rem; color: #0f172a; margin: 0 0 0.2rem 0;",
userDetail: "font-size: 0.8rem; color: #64748b; margin: 0;",
successBadge: "display: inline-block; margin-top: 0.35rem; padding: 0.2rem 0.5rem; background-color: #dcfce7; color: #166534; font-size: 0.75rem; font-weight: 600; border-radius: 4px;",
errorText: "color: #dc2626; font-size: 0.85rem; margin-top: 0.5rem;"
};
// 3. VanJS reactive state
const usersList = van.state([]);
const selectedUserId = van.state("");
const selectedUser = van.state(null);
let isFetching = false;
// 4. Invalidate the step until a user is selected
setModelData(false);
// 5. User selection handler, triggered by clicking a row
function onUserSelected(uuid) {
selectedUserId.val = uuid;
const user = usersList.val.find((u) => {
return u.login.uuid === uuid;
});
if (user) {
selectedUser.val = user;
const fullName = `${user.name.first} ${user.name.last}`;
// Store the selected name and data for the other tabs
setModelData(true, {
userName: fullName,
userEmail: user.email,
userCity: user.location?.city,
userAvatar: user.picture?.medium
});
} else {
selectedUser.val = null;
setModelData(false);
}
}
// 6. Build the component with an interactive table
function buildForm() {
const rows = usersList.val.map((u) => {
const fullName = `${u.name.first} ${u.name.last}`;
return tr({
style: () => {
if (selectedUserId.val === u.login.uuid) {
return styles.selectedRow;
}
return styles.row;
},
onclick: () => {
onUserSelected(u.login.uuid);
}
},
td({ style: styles.td },
img({
src: u.picture?.thumbnail,
alt: fullName,
style: styles.tableAvatar
})
),
td({ style: styles.td }, fullName),
td({ style: styles.td }, u.email),
td({ style: styles.td }, `${u.location?.city}, ${u.location?.country}`)
);
});
return div({ style: styles.container },
// Header
div({ style: styles.header },
p({ style: styles.title }, "Responsible User Selection"),
p({ style: styles.subtitle },
"Click a table row to select the responsible user. Fields marked with ",
span({ style: "color: #dc2626;" }, "*"),
" are mandatory."
)
),
// Users table
table({ style: styles.table },
thead(
tr(
th({ style: styles.th }, "Photo"),
th({ style: styles.th }, "Name"),
th({ style: styles.th }, "Email"),
th({ style: styles.th }, "Location")
)
),
tbody(...rows)
),
// Confirmation card for the selected user
() => {
if (!selectedUser.val) {
return null;
}
const u = selectedUser.val;
const fullName = `${u.name.first} ${u.name.last}`;
return div({ style: styles.userCard },
img({
src: u.picture?.medium || u.picture?.thumbnail,
alt: fullName,
style: styles.cardAvatar
}),
div({ style: styles.userInfo },
p({ style: styles.userName }, fullName),
p({ style: styles.userDetail }, `✉ ${u.email}`),
p({ style: styles.userDetail }, `📍 ${u.location?.city}, ${u.location?.country}`),
span({ style: styles.successBadge }, "✓ Parameter 'userName' stored for the other tabs")
)
);
}
);
}
// 7. Fetch the users and paint the component through the callback
async function refreshUsers() {
if (isFetching) {
return;
}
isFetching = true;
selectedUserId.val = "";
selectedUser.val = null;
setModelData(false);
try {
// Request through the 'http' utility (a Nuxt 4 useFetch instance)
const { data, error } = await http("https://randomuser.me/api/?results=8");
if (error && error.value) {
throw new Error(error.value.message || "Could not fetch the users");
}
const payload = data && data.value ? data.value : data;
if (payload && Array.isArray(payload.results)) {
usersList.val = payload.results;
} else {
usersList.val = [];
}
// Call the callback only once the user refresh has completed
callback(buildForm());
} catch (err) {
console.error("Error refreshing the users:", err);
const errorView = div({ style: styles.container },
p({ style: styles.errorText }, `The users could not be loaded: ${err.message || err}`)
);
callback(errorView);
} finally {
isFetching = false;
}
}
// 8. Refresh the users automatically when the wizard tab is entered or refreshed
onStepEnter(() => {
refreshUsers();
});