Description

This example demonstrates how to:

  1. Fetch data from an external API (randomuser.me) using the platform’s http utility (Nuxt 4 useFetch instance).
  2. Automatically refresh users whenever the tab is entered or refreshed by utilizing the onStepEnter hook, without requiring a manual reload button.
  3. Correctly build and populate the <select> dropdown elements using VanJS (...userOptions), avoiding invalid array returns from reactive binding functions.
  4. Paint the VanJS form exclusively when refreshing/loading users by calling callback(formElement) within the refresh routine and never before.
  5. Pass the selected user’s full name to subsequent wizard tabs using setModelData(true, { userName: ... }).
  6. 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();
});