Example 2
Description
This example demonstrates how to build a Custom Form Tab with VanJS that:
- Captures device identification data: Device Identifier, Battery Level (%), and Storage (GB).
- Validates against OpenGate in real time (using
$api.entitiesSearchBuilder()) to verify that the device exists in the platform. - Dynamically validates all inputs and communicates the validity and data to the wizard using
setModelData(isValid, data).
Code
// 1. Import the VanJS tags used below
const { div, span, strong, label, input, button, p, small } = van.tags;
// 2. Take the default identifier when entityData already carries one
const defaultDeviceId = entityData?.['provision.administration.identifier']?._value?._current?.value
|| entityData?.deviceId
|| "";
// 3. VanJS reactive state
const deviceId = van.state(defaultDeviceId);
const battery = van.state("");
const storage = van.state("");
const isValidating = van.state(false);
const deviceValidated = van.state(false);
const validatedEntity = van.state(null);
const deviceError = van.state("");
const batteryError = van.state("");
const storageError = van.state("");
const statusMessage = van.state("");
// 4. Reusable inline styles
const styles = {
container: "padding: 1rem; font-family: system-ui, -apple-system, sans-serif; color: #1e293b; max-width: 580px;",
fieldGroup: "margin-bottom: 1.25rem;",
label: "display: block; font-weight: 600; font-size: 0.875rem; margin-bottom: 0.35rem; color: #334155;",
inputRow: "display: flex; gap: 0.5rem;",
input: "width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 0.875rem; box-sizing: border-box;",
button: "padding: 0.5rem 1rem; background-color: #2563eb; color: white; border: none; border-radius: 6px; font-size: 0.875rem; font-weight: 500; cursor: pointer; white-space: nowrap;",
buttonDisabled: "padding: 0.5rem 1rem; background-color: #94a3b8; color: white; border: none; border-radius: 6px; font-size: 0.875rem; font-weight: 500; cursor: not-allowed; white-space: nowrap;",
errorText: "color: #dc2626; font-size: 0.8rem; margin-top: 0.3rem; margin-bottom: 0;",
successCard: "background-color: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 6px; padding: 0.75rem; margin-top: 0.5rem; font-size: 0.85rem; color: #166534;",
infoText: "color: #64748b; font-size: 0.8rem; margin-top: 0.25rem; margin-bottom: 0;"
};
// 5. Update the overall validity and send the data to the wizard
function updateModelValidity() {
const isBatValid = validateBattery(battery.val);
const isStorageValid = validateStorage(storage.val);
const isDevValid = deviceValidated.val === true && !deviceError.val;
const isAllValid = isDevValid && isBatValid && isStorageValid;
if (isAllValid) {
setModelData(true, {
deviceId: deviceId.val.trim(),
battery: Number(battery.val),
storage: Number(storage.val),
deviceDetails: validatedEntity.val
});
} else {
setModelData(false);
}
}
// 6. Per-field validation
function validateBattery(val) {
if (val === "" || val === null || val === undefined) {
batteryError.val = "The battery level is mandatory.";
return false;
}
const num = Number(val);
if (isNaN(num) || num < 0 || num > 100) {
batteryError.val = "The battery must be a percentage between 0 and 100.";
return false;
}
batteryError.val = "";
return true;
}
function validateStorage(val) {
if (val === "" || val === null || val === undefined) {
storageError.val = "The storage capacity is mandatory.";
return false;
}
const num = Number(val);
if (isNaN(num) || num <= 0) {
storageError.val = "The storage must be a number greater than 0 GB.";
return false;
}
storageError.val = "";
return true;
}
// 7. Asynchronous validation against OpenGate through $api
async function validateDeviceInOpenGate(targetId) {
const id = (targetId || "").trim();
if (!id) {
deviceError.val = "Please enter a device identifier.";
deviceValidated.val = false;
validatedEntity.val = null;
statusMessage.val = "";
updateModelValidity();
return;
}
isValidating.val = true;
deviceError.val = "";
statusMessage.val = `Querying OpenGate to verify "${id}"...`;
try {
const response = await $api.entitiesSearchBuilder()
.filter({
eq: {
'provision.administration.identifier': id
}
})
.flattened()
.limit(1)
.build()
.execute();
const entity = response?.data?.entities?.[0];
if (entity) {
deviceValidated.val = true;
deviceError.val = "";
validatedEntity.val = {
identifier: id,
name: entity['provision.device.name']?._value?._current?.value || id,
specificType: entity['provision.device.specificType']?._value?._current?.value || "N/A",
operationalStatus: entity['provision.device.operationalStatus']?._value?._current?.value || "N/A"
};
statusMessage.val = "";
} else {
deviceValidated.val = false;
validatedEntity.val = null;
deviceError.val = `The device "${id}" does not exist in OpenGate.`;
statusMessage.val = "";
}
} catch (err) {
console.error("Error validating against OpenGate:", err);
deviceValidated.val = false;
validatedEntity.val = null;
deviceError.val = `Error querying OpenGate: ${err.message || err}`;
statusMessage.val = "";
} finally {
isValidating.val = false;
updateModelValidity();
}
}
// 8. When entityData already carries an identifier, validate it on start
if (defaultDeviceId) {
validateDeviceInOpenGate(defaultDeviceId);
} else {
setModelData(false);
}
// 9. Render the reactive form with VanJS
return div({ style: styles.container },
div({ style: "margin-bottom: 1.5rem;" },
strong({ style: "font-size: 1.1rem; display: block; margin-bottom: 0.25rem;" }, "Device Data"),
p({ style: styles.infoText },
"Fill in the identification data. Fields marked with ",
span({ style: "color: #dc2626;" }, "*"),
" are mandatory."
)
),
van.tags.form({ onsubmit: (e) => e.preventDefault() },
// Field: device identifier + validation button
div({ style: styles.fieldGroup },
label({ style: styles.label },
"Device Identifier ",
span({ style: "color: #dc2626;" }, "*")
),
div({ style: styles.inputRow },
input({
type: "text",
placeholder: "e.g. DEV-001234",
value: deviceId,
style: styles.input,
oninput: (e) => {
deviceId.val = e.target.value;
// When the identifier changes, drop the previous OpenGate validation
deviceValidated.val = false;
validatedEntity.val = null;
deviceError.val = "";
statusMessage.val = "";
updateModelValidity();
},
onkeydown: (e) => {
if (e.key === "Enter") {
validateDeviceInOpenGate(deviceId.val);
}
}
}),
button({
type: "button",
style: () => isValidating.val ? styles.buttonDisabled : styles.button,
disabled: () => isValidating.val,
onclick: () => validateDeviceInOpenGate(deviceId.val)
}, () => isValidating.val ? "Validating..." : "Validate in OpenGate")
),
// Validation status, error and success messages
() => statusMessage.val ? p({ style: styles.infoText }, statusMessage.val) : null,
() => deviceError.val ? p({ style: styles.errorText }, deviceError.val) : null,
() => deviceValidated.val && validatedEntity.val ? div({ style: styles.successCard },
strong("✓ Device verified in OpenGate"),
div({ style: "margin-top: 0.25rem;" }, `Name: ${validatedEntity.val.name}`),
div(`Type: ${validatedEntity.val.specificType} | Status: ${validatedEntity.val.operationalStatus}`)
) : null
),
// Field: battery (%)
div({ style: styles.fieldGroup },
label({ style: styles.label },
"Battery Level (%) ",
span({ style: "color: #dc2626;" }, "*")
),
input({
type: "number",
min: "0",
max: "100",
step: "1",
placeholder: "0 - 100",
value: battery,
style: styles.input,
oninput: (e) => {
battery.val = e.target.value;
validateBattery(battery.val);
updateModelValidity();
}
}),
p({ style: styles.infoText }, "Current battery charge percentage (0 to 100%)."),
() => batteryError.val ? p({ style: styles.errorText }, batteryError.val) : null
),
// Field: storage (GB)
div({ style: styles.fieldGroup },
label({ style: styles.label },
"Storage (GB) ",
span({ style: "color: #dc2626;" }, "*")
),
input({
type: "number",
min: "1",
step: "1",
placeholder: "e.g. 64",
value: storage,
style: styles.input,
oninput: (e) => {
storage.val = e.target.value;
validateStorage(storage.val);
updateModelValidity();
}
}),
p({ style: styles.infoText }, "Total internal storage capacity in gigabytes."),
() => storageError.val ? p({ style: styles.errorText }, storageError.val) : null
)
)
);