TwinaForms Help
Using Custom JavaScript in TwinaForms.
Custom JavaScript lets you add browser-side automation to a published TwinaForms form. Use it for advanced field syncing, light custom behavior, and interaction patterns that go beyond the standard builder settings.
Where to add it
In TwinaForms Designer:
- Open the form.
- Open Form Settings.
- Open Advanced.
- Click Edit Custom JavaScript.
- Paste your code and save.
- Republish the form.
Basic pattern
Most scripts use the same simple structure:
(function (TwinaForms) {
function syncSomething() {
const source = TwinaForms.getValue("text5") || "";
TwinaForms.setValue("text6", source);
}
TwinaForms.on("change", function () {
syncSomething();
});
syncSomething();
})(TwinaForms);
That gives you two important behaviors:
- the script reacts when the user changes a field
- the form also starts in the correct state on first load
Writing several functions
A form can have as many Custom JavaScript functions as you need. Two patterns work well — pick whichever fits the code you are writing.
Pattern A — one IIFE, multiple helpers
Use this when your functions share helpers or need to run together on the same event.
(function (TwinaForms) {
function updatePhoneCode() { /* ... */ }
function updateFullName() { /* ... */ }
function updateSfDatetime() { /* ... */ }
function syncAll() {
updatePhoneCode();
updateFullName();
updateSfDatetime();
}
TwinaForms.on("change", syncAll);
syncAll(); // initial state
})(TwinaForms);
Pattern B — separate IIFEs per concern
Use this when the concerns are unrelated. Each block is self-contained, so you can copy, paste, or remove one without breaking the others.
// Block 1: phone code sync
(function (TwinaForms) {
function updatePhoneCode() { /* ... */ }
TwinaForms.on("change", updatePhoneCode);
updatePhoneCode();
})(TwinaForms);
// Block 2: GMT-to-SF datetime
(function (TwinaForms) {
function updateSfDatetime() { /* ... */ }
TwinaForms.on("change", updateSfDatetime);
updateSfDatetime();
})(TwinaForms);
Examples
These examples use internal field IDs like text5, text6, and checkbox11. Use the actual field IDs from your form.
Build full name from two fields
Assume text1 is first name, text2 is last name, and text3 is full name.
(function (TwinaForms) {
function updateFullName() {
const first = (TwinaForms.getValue("text1") || "").trim();
const last = (TwinaForms.getValue("text2") || "").trim();
const fullName = [first, last].filter(Boolean).join(" ");
TwinaForms.setValue("text3", fullName);
}
TwinaForms.on("change", function () {
updateFullName();
});
updateFullName();
})(TwinaForms);
Set a greeting based on country
Assume text5 is country and text7 is greeting.
(function (TwinaForms) {
function updateGreeting() {
const country = TwinaForms.getValue("text5") || "";
if (country === "Israel") {
TwinaForms.setValue("text7", "Shalom");
return;
}
if (country === "Spain") {
TwinaForms.setValue("text7", "Hola");
return;
}
TwinaForms.setValue("text7", "Hello");
}
TwinaForms.on("change", function () {
updateGreeting();
});
updateGreeting();
})(TwinaForms);
Country to phone country code
Assume text5 is the country field and text6 is the phone code field.
(function (TwinaForms) {
const phoneCodes = {
"Israel": "+972",
"United States": "+1",
"Canada": "+1",
"United Kingdom": "+44",
"Germany": "+49",
"France": "+33",
"Spain": "+34",
"Italy": "+39"
};
function updatePhoneCode() {
const country = TwinaForms.getValue("text5") || "";
TwinaForms.setValue("text6", phoneCodes[country] || "");
}
TwinaForms.on("change", function () {
updatePhoneCode();
});
updatePhoneCode();
})(TwinaForms);
Show a section only when a checkbox is checked
Assume checkbox11 controls a section whose element id is sectionPromo.
(function (TwinaForms) {
function syncSection() {
const checked = TwinaForms.getValue("checkbox11") === true;
if (checked) {
TwinaForms.showElement("sectionPromo");
} else {
TwinaForms.hideElement("sectionPromo");
}
}
TwinaForms.on("change", function () {
syncSection();
});
syncSection();
})(TwinaForms);
Load a special font
Use this when a customer needs a font that is not available in the Theme font picklist. Keep a safe fallback font such as Arial or Tahoma in the theme, then use JavaScript to override the selected Theme font on the published form.
(function () {
const fontName = "Heebo";
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "https://fonts.googleapis.com/css2?family=Heebo:wght@400;600;700&display=swap";
document.head.appendChild(link);
document.documentElement.style.setProperty(
"--nf-font",
"'" + fontName + "', Arial, sans-serif"
);
document.documentElement.style.setProperty(
"--nf-title-font",
"'" + fontName + "', Arial, sans-serif"
);
document.documentElement.style.setProperty(
"--nf-section-font",
"'" + fontName + "', Arial, sans-serif"
);
})();
In this example, Heebo replaces the Theme font for normal text, titles, and section headings on the published form. To use another font, replace both fontName and the Google Fonts URL. The Designer preview can still show the fallback font, and the visitor's browser must be allowed to load the font URL.
GMT with Salesforce — write an SF Datetime value
Detects the submitter's current timezone (including Daylight Saving automatically), combines it with a Date field and a Time field, and writes an ISO 8601 string like 2026-04-23T14:30:00+03:00 into a hidden Text field. Map that hidden field to a Salesforce Datetime column on the Submit action. SF stores it in UTC and renders in each user's locale.
Assume date1 is the date the user picks, time1 is a TwinaForms Time field (HH:mm), and text5 is the hidden Text field submitted to SF.
(function (TwinaForms) {
function pad(n) {
return String(n || 0).padStart(2, "0");
}
function offsetPart() {
var minutes = -new Date().getTimezoneOffset();
var sign = minutes >= 0 ? "+" : "-";
var m = Math.abs(minutes);
return sign + pad(Math.floor(m / 60)) + ":" + pad(m % 60);
}
function datePart() {
var val = TwinaForms.getValue("date1");
if (val) return String(val);
var d = new Date();
return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate());
}
function timePart() {
var val = TwinaForms.getValue("time1");
if (val) {
var p = String(val).split(":");
return pad(p[0]) + ":" + pad(p[1]) + ":" + pad(p[2]);
}
var d = new Date();
return pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds());
}
function updateSfDatetime() {
var iso = datePart() + "T" + timePart() + offsetPart();
TwinaForms.setValue("text5", iso);
}
TwinaForms.on("input", updateSfDatetime);
TwinaForms.on("change", updateSfDatetime);
updateSfDatetime();
})(TwinaForms);
Notes:
- Half-hour timezones (India +05:30, Iran +03:30, Newfoundland -03:30) render correctly.
- Privacy-hardened browsers (Tor, Firefox with privacy.resistFingerprinting) always report UTC. The script still produces valid ISO; it just records +00:00 for those users.
- The Time field returns HH:mm. The script adds seconds as :00 when needed.
- If the Time field is empty, the script uses the current browser time on page load or field change.
- To store midnight instead of the current time when the Time field is empty, replace the last three lines of timePart() with return "00:00:00";.
Conditional total from a Records List — a SUMIF
A plain total is a formula: SUM({rows.score_q}) adds a whole column. What a formula cannot yet do is add only the rows that meet a condition, and that is what this script is for.
Here score_q is the column being added, status_q is the column tested, and only rows reading Approved count towards score_calculated. Set GROUP_KEY to the Records List field key if you know it, to keep the watcher scoped to that list.
(function (TwinaForms) {
const ROW_VALUE = "score_q"; // column to add up
const ROW_TEST = "status_q"; // column to test
const MATCH_VALUE = "Approved"; // only rows matching this count
const TOTAL_FIELD = "score_calculated"; // field holding the total
const GROUP_KEY = ""; // optional, e.g. "rows11"
let writing = false;
function toNumber(raw) {
const n = parseFloat(String(raw == null ? "" : raw).replace(/[^0-9.\-]/g, ""));
return isNaN(n) ? 0 : n;
}
function container() {
return (GROUP_KEY && document.querySelector("[data-repeat-group='" + GROUP_KEY + "']")) || document;
}
function cellValue(row, fieldKey) {
const el = row.querySelector("[data-repeat-field-key='" + fieldKey + "']");
if (!el) { return null; }
if (el.type === "checkbox") { return el.checked ? "true" : "false"; }
return String(el.value == null ? "" : el.value).trim();
}
function recalc() {
if (writing) { return; }
let total = 0;
// Walk ROWS, not cells: the value and the test have to come from the same row.
container().querySelectorAll("[data-repeat-row]").forEach(function (row) {
const test = cellValue(row, ROW_TEST);
if (test === null || test.toLowerCase() !== String(MATCH_VALUE).toLowerCase()) { return; }
total += toNumber(cellValue(row, ROW_VALUE));
});
if (toNumber(TwinaForms.getValue(TOTAL_FIELD)) === total) { return; }
writing = true;
try { TwinaForms.setValue(TOTAL_FIELD, total); } finally { writing = false; }
}
["input", "change"].forEach(function (evt) {
document.addEventListener(evt, function (e) {
const t = e.target;
if (t && t.matches && t.matches("[data-repeat-row] [data-repeat-field-key]")) { recalc(); }
}, true);
});
new MutationObserver(recalc).observe(
container() === document ? document.body : container(),
{ childList: true, subtree: true }
);
TwinaForms.on("prefill:after", recalc);
TwinaForms.on("form:ready", recalc);
recalc();
})(TwinaForms);
Four details make this work, and each one catches people out:
- Walk rows, not cells. The value and the test must come from the same row, so the loop goes over [data-repeat-row] and reads both columns from each one.
- Do not use getAll(). Every row shares one field name, so it returns the first row only.
- setValue() raises a change event, so writing the total would call your own handler again. The writing flag and the equality check stop that loop.
- Adding or deleting a row raises no event, so the script watches the list instead.
To total every row regardless of condition, do not use this — use the formula SUM({rows.score_q}), which recalculates on its own.
Good practices
- Keep scripts short and focused.
- Prefer TwinaForms.getValue() and TwinaForms.setValue() over direct DOM selectors.
- Republish after saving new code.
- Test with realistic form values.
Avoid
- very large scripts
- code that depends heavily on the page HTML structure
- untrusted copied code