Example 3
Description
This example demonstrates how to:
- Fetch data from an external API (randomuser.me) using the platform’s
httputility (Nuxt 4useFetchinstance). - Automatically refresh users whenever the tab is entered or refreshed by utilizing the
onStepEnterhook, without requiring a manual reload button. - Correctly build and populate the
<select>dropdown elements using VanJS (...userOptions), avoiding invalid array returns from reactive binding functions. - 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, label, select, option, 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: 580px;",
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;",
select: "width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 0.875rem; background-color: white; color: #1e293b; box-sizing: border-box;",
userCard: "margin-top: 1rem; padding: 1rem; border: 1px solid #e2e8f0; border-radius: 8px; background-color: #f8fafc; display: flex; gap: 1rem; align-items: center;",
avatar: "width: 56px; height: 56px; 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
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 VanJS component with the select options already resolved
function buildForm() {
const userOptions = usersList.val.map((u) => {
return option(
{ value: u.login.uuid },
`${u.name.first} ${u.name.last} (${u.email})`
);
});
return div({ style: styles.container },
// Header
div({ style: styles.header },
p({ style: styles.title }, "Responsible User Selection"),
p({ style: styles.subtitle },
"Fetches users from an external service to pass the selected name as a parameter to the following steps."
)
),
// User selector
div(
label({ style: "display: block; font-weight: 600; font-size: 0.85rem; margin-bottom: 0.35rem; color: #334155;" },
"User ",
span({ style: "color: #dc2626;" }, "*")
),
select({
style: styles.select,
value: selectedUserId,
onchange: (e) => {
onUserSelected(e.target.value);
}
},
option({ value: "" }, "-- Select a user --"),
...userOptions
)
),
// Preview 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.avatar
}),
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();
});