The Short Version
EJS reads form data
In HTML Blocks, emails, and templates, read values from locals.
<%= locals.companyName || 'Customer' %>
Events change form data
If a value must be saved, calculated, normalized, or prefilled, use Form Events.
setValue('service', 'Hotshot');
Do not mix them up
EJS is for display. Form Events are for mutation. This single rule prevents most Wizara issues.
Events write -> locals update -> EJS renders
Copy/Paste Starter
This is the safest starting point for an HTML Block or email template. It handles empty values, arrays, dates, addresses, file uploads, and unknown objects without showing [object Object].
EJS starter helpers
<%
const hiddenKeys = ['_errors', 'internalNotes', 'helperField'];
function hasValue(value) {
return value !== undefined && value !== null && value !== '';
}
function parseLocalDate(value) {
if (Object.prototype.toString.call(value) === '[object Date]' && !isNaN(value)) {
return value;
}
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value || ''));
if (match) return new Date(+match[1], +match[2] - 1, +match[3]);
const date = new Date(value);
return isNaN(date) ? null : date;
}
function formatDate(value) {
const date = parseLocalDate(value);
if (!date) return '';
return date.toLocaleDateString('en-CA', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
function formatAddress(value) {
if (!value || typeof value !== 'object') return '';
return [
value.address,
value.locality,
value.regionCode ? String(value.regionCode).replace(/^CA-/, '') : '',
value.postalCode
].filter(Boolean).join(', ');
}
function formatFile(value) {
if (!value || typeof value !== 'object') return '';
return value.name || value.fileName || value.filename || '';
}
function formatValue(value) {
if (!hasValue(value)) return '';
if (Array.isArray(value)) {
return value.map(formatValue).filter(Boolean).join(', ');
}
if (Object.prototype.toString.call(value) === '[object Date]') {
return formatDate(value);
}
if (typeof value === 'object') {
const address = formatAddress(value);
if (address) return address;
const file = formatFile(value);
if (file) return file;
return JSON.stringify(value);
}
if (value === true) return 'Yes';
if (value === false) return 'No';
return String(value);
}
function labelFromKey(key) {
return String(key)
.replace(/([a-z])([A-Z])/g, '$1 $2')
.replace(/_/g, ' ')
.replace(/\b\w/g, (char) => char.toUpperCase())
.replace(/\bBol\b/g, 'BOL')
.replace(/\bDob\b/g, 'DOB')
.replace(/\bVin\b/g, 'VIN')
.replace(/\bSin\b/g, 'SIN');
}
const rows = Object.entries(locals)
.filter(([key, value]) => !hiddenKeys.includes(key) && hasValue(value));
%>
Universal summary table
<table width="100%" cellpadding="8" cellspacing="0" style="border-collapse:collapse;">
<% for (const [key, value] of rows) { %>
<tr>
<td style="border-top:1px solid #ddd; width:35%;">
<strong><%= labelFromKey(key) %></strong>
</td>
<td style="border-top:1px solid #ddd;">
<%= formatValue(value) %>
</td>
</tr>
<% } %>
</table>
Why this starter matters: real Wizara submissions often include arrays, address objects, upload objects, dates, booleans, and helper fields. A template that only prints strings will eventually break or show ugly output.
Core EJS Syntax
Print a value
<p>Hello, <%= locals.firstName || 'there' %>.</p>
Show a section only when a field has data
<% if (locals.message) { %>
<p><strong>Message:</strong> <%= locals.message %></p>
<% } %>
Use bracket syntax for unusual field names
<%= locals["_16WheelerYears"] || '' %>
Avoid duplicate variable names
<% { const title = 'Driver Summary'; %>
<h3><%= title %></h3>
<% } %>
Pattern: Review Card
Use this near the end of a form to help the user double-check key answers before submitting.
<%
const name = locals.fullName || locals.name || 'Customer';
const company = locals.company || '';
const service = locals.service || locals.service_type || '';
const pickup = formatAddress(locals.pickupAddress);
const delivery = formatAddress(locals.deliveryAddress);
%>
<div style="border:1px solid #ddd; padding:16px; background:#f8fafc;">
<h3 style="margin-top:0;">Review Your Request</h3>
<p><strong>Name:</strong> <%= name %></p>
<% if (company) { %><p><strong>Company:</strong> <%= company %></p><% } %>
<% if (service) { %><p><strong>Service:</strong> <%= service %></p><% } %>
<% if (pickup) { %><p><strong>Pickup:</strong> <%= pickup %></p><% } %>
<% if (delivery) { %><p><strong>Delivery:</strong> <%= delivery %></p><% } %>
</div>
Pattern: Dynamic Risk or Score Card
EJS can display a score. If the score must be submitted, calculate it in an On Field Value Change event and store it in a field.
HTML Block EJS
<%
const p = Number(locals.probability) || 0;
const e = Number(locals.exposure) || 0;
const ih = Number(locals.impactHuman) || 0;
const ifi = Number(locals.impactFinancial) || 0;
const total = p + e + ih + ifi;
let rating = 'No Risk Rating';
if (total >= 10) rating = 'High';
else if (total >= 7) rating = 'Medium';
else if (total >= 4) rating = 'Low';
%>
<div style="padding:16px; border:1px solid #ddd;">
<h3 style="margin:0;"><%= rating %></h3>
<p style="font-size:32px; margin:8px 0 0;"><%= total %></p>
</div>
On Field Value Change if the score must be stored
async (fieldName, oldValue, newValue, values, setValues) => {
let p = toNumber(values.probability);
let e = toNumber(values.exposure);
let ih = toNumber(values.impactHuman);
let ifi = toNumber(values.impactFinancial);
switch (fieldName) {
case 'probability': p = toNumber(newValue); break;
case 'exposure': e = toNumber(newValue); break;
case 'impactHuman': ih = toNumber(newValue); break;
case 'impactFinancial': ifi = toNumber(newValue); break;
}
const total = p + e + ih + ifi;
setValues((prev) => ({
...prev,
riskRating: `${total}`
}));
function toNumber(value) {
const number = Number(value);
return isNaN(number) ? 0 : number;
}
};
Important: during On Field Value Change, values still contains the old value for the field that just changed. Use newValue for the active field or the total will be one step behind.
Pattern: Date Confirmation Without Timezone Drift
Use a local parser for YYYY-MM-DD values. This prevents dates from showing one day early or late.
<%
function parseLocalDate(value) {
if (Object.prototype.toString.call(value) === '[object Date]' && !isNaN(value)) {
return value;
}
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value || ''));
if (match) return new Date(+match[1], +match[2] - 1, +match[3]);
const date = new Date(value);
return isNaN(date) ? null : date;
}
function getOrdinal(day) {
const suffixes = ['th', 'st', 'nd', 'rd'];
const value = day % 100;
return day + (suffixes[(value - 20) % 10] || suffixes[value] || suffixes[0]);
}
function formatLongDate(value) {
const date = parseLocalDate(value);
if (!date) return '';
const weekday = date.toLocaleDateString('en-US', { weekday: 'long' });
const month = date.toLocaleDateString('en-US', { month: 'long' });
return `${weekday}, ${month} ${getOrdinal(date.getDate())}, ${date.getFullYear()}`;
}
const leaveDate = formatLongDate(locals.leaveDate);
const returnDate = formatLongDate(locals.returnDate);
%>
<% if (leaveDate && returnDate) { %>
<div style="padding:14px; background:#23333E; color:#fff; text-align:center;">
Away <strong><%= leaveDate %></strong> and back <strong><%= returnDate %></strong>.
</div>
<% } %>
Pattern: Custom Validation Message
EJS can show a helpful message, but it does not block submission by itself. Use this for clear guidance, and avoid duplicate error boxes by checking locals._errors.
<%
const email = typeof locals.email === 'string' ? locals.email.trim() : '';
const confirmEmail = typeof locals.confirmEmail === 'string' ? locals.confirmEmail.trim() : '';
const showEmailError =
!(locals._errors.email || []).length &&
!(locals._errors.confirmEmail || []).length &&
email &&
confirmEmail &&
email !== confirmEmail;
%>
<% if (showEmailError) { %>
<div class="WZ__Form__errors alert alert-danger">
<ul class="WZ__Form__errors-list">
<li class="WZ__Form__errors-item">Your email addresses do not match.</li>
</ul>
</div>
<% } %>
Pattern: Job or Service Redirect Notice
Use a Form Event to classify the request, write helper fields, then use EJS to show the notice. This keeps the display simple and the logic testable.
HTML Block EJS
<%
const intent = locals.lookslikejobrequest || '';
const message = locals.jobRedirectMessage || '';
const url = locals.jobRedirectUrl || '';
%>
<% if ((intent === 'strong' || intent === 'possible') && url) { %>
<div style="padding:14px; border-left:4px solid #c21f32; background:#fff7f7;" role="status" aria-live="polite">
<p style="margin:0 0 8px;"><strong><%= message || 'This may belong in another form.' %></strong></p>
<a href="<%= url %>" target="_top">Open the correct form</a>
</div>
<% } %>
Use target="_top" when the link must escape an embedded iframe and replace the full browser page.
Pattern: Confirmation Email
Email clients are stricter than normal web pages. Use tables, inline styles, hosted images, and simple conditionals.
<%
const name = locals.fullName || locals.name || 'there';
const service = locals.service || locals.service_type || '';
const message = locals.message || '';
%>
<table width="100%" cellpadding="0" cellspacing="0" style="font-family:Arial,sans-serif; color:#222;">
<tr>
<td style="padding:20px; background:#f8fafc;">
<h2 style="margin:0 0 8px;">Thanks, <%= name %>.</h2>
<p style="margin:0;">We received your submission.</p>
</td>
</tr>
<% if (service) { %>
<tr>
<td style="padding:14px 20px; border-top:1px solid #ddd;">
<strong>Service:</strong> <%= service %>
</td>
</tr>
<% } %>
<% if (message) { %>
<tr>
<td style="padding:14px 20px; border-top:1px solid #ddd;">
<strong>Message:</strong><br>
<%= message %>
</td>
</tr>
<% } %>
</table>
Email rule: avoid relying on flexbox in confirmation emails. Tables and inline styles are more dependable.
Pattern: Prefilled Form Values and EJS
EJS can display a prefilled value, but it should not do the prefill. Use On Form Load with setValue, then EJS can read the result from locals.
On Form Load
async (setValue, setValues) => {
const allowedServices = ['FTL / Full Deck', 'Dry Bulk', 'Hotshot', 'Not sure'];
const applyValues = (data) => {
if (!data || typeof data !== 'object') return;
const service = String(data.service || '').trim();
if (allowedServices.includes(service)) {
setValue('service_type', service);
}
};
const params = new URLSearchParams(window.location.search);
applyValues({ service: params.get('service') });
window.addEventListener('message', (event) => {
const data = event.data && typeof event.data === 'object' ? event.data : null;
if (data?.wizaraPopulateQuote) applyValues(data);
});
};
HTML Block EJS
<% if (locals.service_type) { %>
<p>Selected service: <strong><%= locals.service_type %></strong></p>
<% } %>
What EJS Is Good For
- Live review summaries
- Risk and score displays
- Conditional warnings
- Confirmation email layouts
- Printable compliance sections
- Human-readable address and upload output
- Dynamic thank-you copy
- Showing helper-field results
- Displaying prefilled service or campaign context
- Hiding internal fields from emails
- Turning field keys into readable labels
- Checking native errors before showing extra guidance
What EJS Should Not Do
Do not use EJS to change values
EJS does not persist submitted data. If a value needs to be saved, use setValue or setValues in a Form Event.
Do not use EJS for URL prefill
Use On Form Load to read the URL or listen for parent-page postMessage. EJS can display the value after it exists.
Do not render unknown objects directly
Objects need deliberate formatting. Otherwise the user may see [object Object].
Do not guess field names
Field names, casing, option labels, and Unicode characters must match the current export.
Testing Checklist
- Export the latest form JSON before writing EJS.
- Confirm exact field names and option labels.
- Test with blank values, single values, arrays, address objects, and uploaded files.
- Check dates that come from date fields, especially around month boundaries.
- Check the email output separately from the live form preview.
- If using an iframe prefill, test the raw Wizara embed first, then the published website page.
- Do not add styling until the data renders correctly.
- Keep one known-good export before every major revision.
Final Rule
If the form needs to show something, EJS is probably right. If the form needs to save, change, calculate, prefill, clear, or normalize something, use Form Events first and let EJS display the result.