Custom JS — Window API Reference
Professional & EnterpriseDocuments all functions and variables exposed to window by mDFFS for use in Custom JS. Every function is set when the form loads and deleted when it unloads. In multi-panel scenarios the last loaded panel wins.
Callable Functions
dffs_triggerSave(exit, autosave, goToEditForm)
Programmatically saves the form.
exit(boolean) — close the panel after saveautosave(boolean) — skip save if the user has made no interactionsgoToEditForm(boolean) — redirect to the edit form after save
dffs_triggerSave(true, false, false); // save and close
dffs_triggerSave(false, false, false); // save and stay opendffs_cancelForm()
Closes/cancels the form without saving. Equivalent to clicking the Cancel button.
dffs_cancelForm();getFieldValue(fin, property?)
Returns the current value of a field. fin = InternalName.
| Field type | property | Returns |
|---|---|---|
| Any | (omit) | Formatted display value (string) |
| Text / Note (FK2/3) rendered as an autocomplete search box | (omit) | The selected text — what is saved to the item. Multiple values from a multi-value Multiple lines of text field are separated by newlines; a cleared selection is "". (Before 2.6 this returned the search box’s option objects once a value had been picked.) |
| DateTime (FK4) | "dateObject" | JS Date object |
| DateTime (FK4) | "dateString" | ISO string without milliseconds |
| DateTime (FK4) | "formattedDate:MM/dd/yyyy" | Formatted date string |
| Lookup (FK7) | "LookupId" | ID(s): int for single-value, int[] for multi-value (empty multi = [], empty single = "") |
| Lookup (FK7) | (omit) | Display value(s) |
| Boolean (FK8) | "boolean" | true / false |
| Boolean (FK8) | (omit) | "Yes" / "No" string |
| Number / Currency (FK9/10) | "float" | Raw number |
| Hyperlink (FK11) | "Url" | URL string |
| Hyperlink (FK11) | "Description" | Description string |
| Attachments (FK19) | (omit) | [{FileName, ServerRelativeUrl}] |
| Person (FK20) | "email" | Email address |
| Person (FK20) | "title" | Display name |
| Person (FK20) | "id" | User ID |
| Person (FK20) | "accountname" | Login name |
| Special: item ID | fin = "Id" or "ID" | Current item ID (number) |
getFieldValue("Title");
getFieldValue("StartDate", "dateObject");
getFieldValue("AssignedTo", "email");
getFieldValue("Id"); // item IDsetFieldValue(fin, value)
Sets a field’s value programmatically. Returns a Promise. DFFS tokens are resolved in string values.
| Field type | Value format |
|---|---|
| Text / Multiline (FK2/FK3) | string |
| DateTime (FK4) | JS Date object |
| Choice (FK6) | string — exact choice value |
| Lookup (FK7) single | lookup ID or display value as string |
| Lookup (FK7) multi | comma-separated IDs/values, or array |
| Boolean (FK8) | true / false |
| Taxonomy / MM (FK0/FK15) | term name string, or array of term name strings |
| Person (FK20) | login name string, or array of login name strings |
| Clear any field | "" or null |
await setFieldValue("Title", "New title");
await setFieldValue("StartDate", new Date());
await setFieldValue("Status", "Approved");
await setFieldValue("AssignedTo", "i:0#.f|membership|user@domain.com");
await setFieldValue("Tags", ["Finance", "Q1"]);
await setFieldValue("Status", ""); // clear fielddffs_setDateRestriction(fin, minDate, maxDate)
Restricts the selectable range in a Date / Date & Time field’s picker at runtime. Takes plain JavaScript Date objects (not the mode-based configuration used by the field properties panel and the rule action). Pass null or undefined for either bound to leave that side unrestricted. An override set here takes precedence over the field’s own static Minimum/Maximum date configuration while it is in effect.
fin— InternalName of the target Date / Date & Time fieldminDate— earliest selectable date as aDate, ornull/undefinedfor no lower boundmaxDate— latest selectable date as aDate, ornull/undefinedfor no upper bound
// Keep EndDate on or after the currently chosen StartDate, with no upper bound
const start = getFieldValue("StartDate", "dateObject");
dffs_setDateRestriction("EndDate", start, null);dffs_clearDateRestriction(fin)
Removes a restriction previously set with dffs_setDateRestriction and reverts the field to its own static Minimum/Maximum date configuration (if any).
fin— InternalName of the target Date / Date & Time field
dffs_clearDateRestriction("EndDate");dffs_showModal({title, body, isBlocking, …})
Shows a modal dialog. Returns the modalId string.
| Property | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Modal title |
body | string / HTML | Yes | Modal body content |
isBlocking | boolean | Yes | Prevent clicking outside to close |
modalId | string | No | Custom ID; auto-generated if omitted |
okBtnFn | function | No | OK button callback |
okBtnLabel | string | No | OK button label |
cancelBtnFn | function | No | Cancel button callback |
cancelBtnLabel | string | No | Cancel button label |
closeCallbackFn | function | No | Called when the modal closes |
showClose | boolean | No | Show X close button |
showFooter | boolean | No | Show footer area |
const modalId = dffs_showModal({
title: "Confirm action",
body: "<p>Are you sure you want to submit?</p>",
isBlocking: true,
okBtnLabel: "Yes, submit",
okBtnFn: () => dffs_triggerSave(true, false, false),
cancelBtnLabel: "Cancel"
});dffs_hideModal(modalId)
Closes a modal by its ID.
dffs_hideModal(modalId);dffs_showPanel({title, body, position, size, …})
Opens the same dialog as dffs_showModal, pinned to an edge of the screen instead of centered — the right shape for reference information or instructions a user needs to read while still seeing the form behind it. It takes every dffs_showModal option, plus the two below, and returns the modalId string.
| Property | Type | Required | Description |
|---|---|---|---|
position | string | No | "right" (default), "left", "top" or "bottom". Any other value is treated as "right". |
size | number / string | No | The panel’s width for left and right, or its height for top and bottom — a number of pixels or any CSS length ("40%", "30rem"). Defaults to 420 wide or 320 tall. |
isBlocking | boolean | No | Defaults to false here, unlike dffs_showModal: clicking back onto the form closes the panel. Pass true to keep it open until it is closed explicitly. |
const panelId = dffs_showPanel({
title: "Order details",
body: "<p>Anything a modal can hold.</p>",
position: "right",
size: 520
});A panel carries dffs-panel and dffs-panel-right / -left / -top / -bottom next to the usual dffs-modal, so Custom CSS can target all panels, one edge, or a single panel through its modalId. Its thickness is the --dffs-panel-size CSS variable — see Custom CSS.
dffs_hidePanel(panelId)
Closes a panel by its ID. It is the same function as dffs_hideModal, so either one closes either kind of dialog.
dffs_hidePanel(panelId);dffs_showDialog({title, body, …})
Shows the simple message dialog mDFFS uses internally. Only title and body are required. An OK button is always shown and closes the dialog; a Cancel button appears only if you supply cancelBtnFn.
Unlike dffs_showModal it returns nothing — use dffs_showModal / dffs_hideModal when you need to close a dialog programmatically.
| Property | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Dialog title |
body | string | Yes | Body content — a plain string, rendered as HTML, so basic markup works |
subText | string | No | Secondary text below the title |
width | number | No | Dialog width in px (default 450) |
isBlocking | boolean | No | Default false; true prevents dismissing by clicking outside |
okBtnFn | function | No | OK button callback |
okBtnLabel | string | No | OK button label |
cancelBtnFn | function | No | Cancel button callback — supplying this is what makes the Cancel button appear |
cancelBtnLabel | string | No | Cancel button label |
closeCallbackFn | function | No | Called when the dialog closes |
labelClose | string | No | aria label for the X button; defaults to the localized Close label |
dffs_showDialog({ title: "Heads up", body: "This item is locked for editing." });dffs_doReadonly(finArr, redraw)
Makes one or more fields read-only at runtime.
finArr: comma-separated string or array of InternalNamesredraw:trueto re-render the form immediately
dffs_doReadonly(["Title", "Status"], true);
dffs_doReadonly("Title,Status", true);dffs_undoReadonly(finArr, redraw)
Removes the read-only state from one or more fields. Same signature as dffs_doReadonly.
dffs_undoReadonly(["Title"], true);dffs_flagMandatory(arr)
Adds fields to the required list at runtime.
arr: array of InternalNames
dffs_flagMandatory(["DueDate", "Approver"]);dffs_clearMandatory(arr)
Removes fields from the required list.
arr: array of InternalNames
dffs_clearMandatory(["DueDate"]);dffs_overrideMandatory(arr)
Sets an override list that shadows the rule-driven required-fields list while arr is non-empty — only the fields in arr are required. Rules keep computing their own required-fields list in the background, so validation can fall back to it later (see dffs_resetMandatoryOverride). Pass an empty array ([]) to clear the override. Native SharePoint Required columns are always enforced regardless of override state. The required-field asterisk still shows for fields that are rule-required even while an active override excludes them from enforcement — the indicator reflects the union of the rule-driven list and the override, so users see everything that will eventually be required, while only the override’s fields block save.
arr: array of InternalNames
dffs_overrideMandatory(["Title"]); // only Title required now
dffs_overrideMandatory([]); // clear the overridedffs_resetMandatoryOverride()
Clears the override set by dffs_overrideMandatory, so validation and the required-field asterisks fall back to whatever the rules currently compute — without you needing to know that list. Equivalent to dffs_overrideMandatory([]).
// While Status != Completed, restrict what's required:
dffs_overrideMandatory(["Title"]);
// When Status == Completed, hand control back to the rules:
dffs_resetMandatoryOverride();dffs_getRequiredFields()
Returns a copy of the current rule-driven base required-fields list (array of InternalNames), unaffected by any active override.
var base = dffs_getRequiredFields(); // e.g. ["Title", "DueDate", "Approver"]dffs_getRequiredFieldsOverride()
Returns a copy of the current override list (as set by dffs_overrideMandatory). An empty array means no override is active.
var override = dffs_getRequiredFieldsOverride(); // [] when no overrideBoth accessors return snapshots (
[...arr]), not live references — the underlying lists are reassigned whenever the rules or the override change, so a value read once and cached will go stale. Call the function again whenever you need the current value.
dffs_getVisibleFields(checkDom)
Returns a copy of the fields the rules have left visible (array of InternalNames), in the order they appear on the form. The companion to dffs_getRequiredFields() — use it to answer “is everything the user can see filled in?” without hard-coding a field list that goes stale the first time someone adds a tab or a hide rule.
window.dffs_ready = function () {
var empty = dffs_getVisibleFields().filter(function (fin) {
var v = getFieldValue(fin);
return v === "" || v === null || v === undefined || v.length === 0;
});
if (empty.length > 0) {
console.log("Not filled in yet: " + empty.join(", "));
}
};The list is built by walking the saved form configuration and applying the same tests the renderer applies. A field is included unless:
- a rule has hidden it (the hiddenFields action),
- it sits inside a tab a rule has switched off (the toggleTab action),
- it is no longer in the list schema — the column was deleted but left on the form, or
- the form is a display form with Hide empty fields on and the field has no value.
Duplicates are collapsed: a field placed twice on the form appears once.
A field on a background tab counts as visible. It is not on screen this second, but the user reaches it by clicking a tab, with nothing else needing to change — and for the question this answers, a field two tabs over is very much part of the ask. It also means the answer does not shift as the user moves between tabs, which would make any check written against it depend on where the user happened to be standing.
The optional DOM pass
The showElement / hideElement rule actions and Custom CSS do not change form state at all — they set style.display on whatever the selector matched, and nothing in the configuration records that it happened. Pass true to add a second pass over the rendered markup:
dffs_getVisibleFields(true)Each field’s wrapper is walked up through its ancestors looking for anything currently display: none or visibility: hidden, which catches a hidden column, row or section, and a field hidden by Custom CSS. The tab content wrapper is deliberately looked past: it is display: none for every unselected tab by design, and without that exception the DOM pass would return only the current tab’s fields.
Call it from
dffs_readyor later. The DOM pass needs the form to have rendered — called earlier it would report every field as hidden, which is a wrong answer that looks like a real one. That is why it is opt-in rather than the default. Where a field has no markup in the document yet, the DOM pass declines to overrule the configuration walk rather than guessing.
- Returns a fresh array on every call, like
dffs_getRequiredFields()— mutating the result cannot reach form state. - Reads Hide empty fields from the live configuration rather than React state, so a call made from a stale closure (a listener registered on an earlier render) still gets the current answer.
- With a vLookup child form open, the DOM pass prefers the elements inside the form that is asking, so a parent and child sharing field names do not answer for each other.
- Repeating section columns are not included. A repeating section holds its own columns rather than list fields, and the section itself is not a field — so a form built entirely from repeating sections returns an empty array.
- There is no matching rule condition. A rule needing the same test can call this from a function action.
dffs_checkMandatory(silent)
Validates all required fields. Returns true if all pass.
silent = false: highlights empty required fields and the tabs containing themsilent = true: validates without any visual feedback
if (!dffs_checkMandatory(false)) {
// stop — form has highlighted the empty required fields
return;
}
dffs_triggerSave(true, false, false);dffs_selectTab(id)
Activates a tab by its pivotItem ID. The "tab_" prefix is optional.
dffs_selectTab("pivotItem_20260502_tab2");
dffs_selectTab("tab_pivotItem_20260502_tab2"); // same resultdffs_triggerRule(ruleId, bool)
Manually fires a rule’s actions.
ruleId: the rule’sidstring ornamestringbool:trueexecutesactionYes;falseexecutesactionNo
dffs_triggerRule("rule_20260502_lasttab", true);
dffs_triggerRule("Show message on last tab selected", false);dffs_addClickEventListener(id, fn)
Adds a delegated click event listener to a DOM element by ID. The listener is attached to the panel container, not the element itself, so it survives re-renders.
dffs_addClickEventListener("my-custom-button", () => {
dffs_triggerSave(true, false, false);
});dffs_addEventListener(id, eventType, fn)
Adds a delegated event listener for any DOM event type. Same delegation model as dffs_addClickEventListener.
dffs_addEventListener("my-input", "focus", () => {
console.log("Field focused");
});dffs_cancel_fieldAsAutocomplete(itemId)
Disables autocomplete mode on a text field that has textIsAutocomplete configured. itemId = field element ID (field_{fin}_{id}) or just the ID part.
dffs_cancel_fieldAsAutocomplete("field_Title_abc123");dffs_cancel_fieldAsDropdown(itemId)
Disables dropdown mode on a text field that has textIsDropdown configured. Same signature.
dffs_cancel_fieldAsDropdown("field_Status_abc123");vLookup_loadItems(id)
Reloads the items in a vLookup component. Accepts the full element ID ("vLookup_xxx") or just the ID part. Returns a Promise.
await vLookup_loadItems("vLookup_abc123");
await vLookup_loadItems("abc123"); // same resultvlookup_open(vLookupId, form, itemId)
Opens a vLookup item form in a panel.
vLookupId: full element ID ("vLookup_xxx") or just the ID partform:"new"or"edit"itemId: SharePoint item ID — required for"edit", ignored for"new"
vlookup_open("vLookup_abc123", "new", 0);
vlookup_open("abc123", "edit", 42);dffs_setPdfSettingsOverride(settings) Print to PDF add-on
Patches the form’s Print to PDF settings at runtime. It’s designed for solutions that load the same DFFS configuration across multiple sites (e.g. from a CDN) where each site still needs to control Print to PDF individually — turn it off entirely, drop the cover page, use its own logo, and so on — typically driven by per-site settings read asynchronously from a local SharePoint list.
settings is merged on top of the PDF settings saved in the config — a patch, not a replacement. The merge is one level deep per section (coverPage, header, content, footer, and the nested content.sectionHeadingStyle), so you pass only the keys a site should change; every other value is kept from the shared config. Each call replaces the previous override rather than accumulating — pass the full set of overrides you want active each time.
Call it any time — there’s no need to wait for dffs_ready. Because it’s a real function call (not a value read lazily off window), it re-renders immediately: the Save-as-PDF button, the mobile menu entry, and the printed output all reflect the new settings the instant it’s called, with no flash of stale state while your async code loads.
Flash-free recommended pattern: ship the shared config with Print to PDF off, then have only the sites that should show it call
dffs_setPdfSettingsOverride({ enabled: true })once their local settings confirm it. Sites that never call it — or call it after a delay — simply never show the button in the meantime.
// Shared config ships with "Enable save as PDF" = OFF. Once this site's local
// settings list confirms it should be on, turn it on for this site only:
dffs_setPdfSettingsOverride({ enabled: true });
// Keep everything from the shared config, just hide the cover page
dffs_setPdfSettingsOverride({ coverPage: { enabled: false } });
// Keep everything, just use this site's own header logo
dffs_setPdfSettingsOverride({
header: { logoUrl: "https://tenant.sharepoint.com/sites/mysite/docs/logo.png?h=50" }
});The full settings shape (matching what the Print to PDF config panel produces):
{
enabled, documentTitle,
coverPage: { enabled, logoUrl, logoHeight, logoAlignment, title, subtitle,
body, backgroundColor, textColor, textAlignment },
header: { enabled, logoUrl, logoHeight, logoAlignment, customHtml },
content: { tabsAsSections,
sectionHeadingStyle: { fontSize, color, backgroundColor,
borderColor, bold, italic },
hideEmptyFields, excludedFields, excludedTabs },
footer: { enabled, text, showPageNumbers, showPrintDate, showPrintedBy }
}SharePoint List CRUD
Read and write any SharePoint list, not just the current form’s list. All are async (return a Promise). list accepts a list title or GUID. The optional siteUrl targets a different site (cross-site).
dffs_spAdd(list, data, siteUrl?)
Adds an item to a list. Returns the new item object (including its Id).
const item = await dffs_spAdd("Tasks", { Title: "New task", Status: "Open" });
console.log(item.Id);dffs_spGet(list, options?, siteUrl?)
Returns an array of items. options: { filter?, select?, expand?, orderBy?, orderAscending?, top? } (select/expand are arrays of internal names).
const items = await dffs_spGet("Tasks", {
filter: "Status eq 'Open'",
select: ["Title", "Status"],
orderBy: "Modified",
orderAscending: false,
top: 50
});dffs_spGetById(list, itemId, options?, siteUrl?)
Returns a single item by ID. options: { select?, expand? }.
const item = await dffs_spGetById("Tasks", 42, { select: ["Title", "Status"] });dffs_spUpdate(list, itemId, data, siteUrl?)
Updates item fields. data = { [internalName]: value }. Returns the PnP update result.
await dffs_spUpdate("Tasks", 42, { Status: "Closed" });dffs_spDelete(list, itemId, siteUrl?)
Recycles an item (sends it to the Recycle Bin). Does not show a confirmation dialog.
await dffs_spDelete("Tasks", 42);Callback Hooks — Define in Custom JS
These are functions you define in Custom JS. mDFFS calls them automatically when the corresponding event occurs.
Each form’s Custom JS is isolated. Callbacks defined in one form’s Custom JS do not carry over to another — for example a
dffs_readydefined only on DispForm will not run after switching to EditForm for the same item. Define the callbacks each form needs in that form’s own Custom JS.
dffs_fieldChanged(fin, newValue, oldValue)
Called whenever any field value changes. Define on window.
- Attachments (from 2.6): called when a file is added or removed, with
fin="Attachments"andnewValue/oldValueas arrays of{FileName, ServerRelativeUrl}— the files after and before the change, including files added in the form that are not saved yet. - A text field rendered as an autocomplete search box:
newValueis the search box’s raw option array, not the text. CallgetFieldValue(fin)inside the handler to get the selected text.
window.dffs_fieldChanged = function(fin, newValue, oldValue) {
if (fin === "Status" && newValue === "Approved") {
dffs_flagMandatory(["ApprovalDate"]);
}
};dffs_vLookup_callback(vLookupId, items)
Called after a vLookup component finishes loading or reloading its items, once the vLookup has rendered — so its rows, or its “no items” message, can be read from the page inside the callback. If the vLookup has a prepare function, the callback runs after it has finished. (In 2.3 and 2.4 the callback could run before the vLookup had rendered.)
vLookupId: full element ID string, e.g."vLookup_abc123"items: array of SharePoint list item objects as returned by the REST API
window.dffs_vLookup_callback = function(vLookupId, items) {
if (vLookupId === "vLookup_abc123") {
console.log("Loaded", items.length, "items");
}
};dffs_repeatingSection_callback(elementId, action, data)
Fires when the user adds, updates or removes a Repeating Section row, or dismisses the inline editor.
elementId:repeatingSection_{id}— the id shown at the bottom of the section’s configuration panelaction:"add"|"update"|"remove"|"cancel"data:{ rowId, values }
window.dffs_repeatingSection_callback = function (elementId, action, data) {
if (action === "add") {
console.log("Row staged on", elementId, data.values);
}
};This reports intent, not persistence. Repeating Section rows are staged and only written to the child list when the parent form is saved. A staged row is discarded if the form is cancelled, and
rowIdis negative for a row added but not yet saved. There is no post-write hook at the commit point.
dffs_vLookup_itemChanged_callback(vLookupId, action, itemId)
Fires when a single vLookup item changes.
vLookupId:vLookup_{id}— the id shown in the vLookup configuration panelaction:"add"|"update"|"remove"|"cancel"itemId: the list item id
window.dffs_vLookup_itemChanged_callback = function (vLookupId, action, itemId) {
console.log(vLookupId, action, itemId);
};Unlike the Repeating Section callback, add / update / remove fire after the write succeeded and before the table reloads — so dffs_vLookup_callback fires immediately afterwards. "cancel" means the child form was closed, or the inline editor dismissed, without saving; closing a display form does not count. Covers all four routes into a vLookup item: the child form panel, inline editing, the table’s delete button, and the add-new-document upload path.
dffs_repeatingSection_loaded_callback(elementId, items)
Called after a Repeating Section’s rows are fetched on form open, and again after every in-memory row change (add, edit or delete) — so it always has the current state of the section, including unsaved local changes.
elementId:repeatingSection_{id}items: the current array of rows. Each row carries its SharePoint field values plusId, and_isNew: trueon locally-added rows not yet saved.
window.dffs_repeatingSection_loaded_callback = function (elementId, items) {
console.log(elementId, "has", items.length, "rows");
};Changed in 2.2:
elementIdnow carries therepeatingSection_prefix, matching the vLookup callbacks, the DOM id and the configuration panel. Custom JS comparing it against an id of your own needs that prefix adding.
vLookup prepare function (name it in the vLookup configuration)
Unlike the other hooks on this page, this one has no fixed name. You write the function in Custom JS and type its name into Name of prepare function on the Table tab of the vLookup’s configuration — the same way a customizer function is named.
It is called once per load with every row the vLookup query returned, before the table is drawn. If it returns a promise, the table stays on its loading spinner until that promise resolves. Use it to fetch anything a customizer function needs but cannot fetch itself, because a customizer must return its HTML synchronously.
items: array of SharePoint list item objects, exactly as the table will render them. Values you attach to a row are visible to the customizer function.config: the vLookup’s configuration object.
async function vLookupResolveOwners(items, config) {
var ids = items.map(function (item) { return item.OwnerId; }).filter(Boolean);
if (ids.length === 0) {
return;
}
// One request for the whole table — not one per row
var filter = ids.map(function (id) { return "Id eq " + id; }).join(" or ");
var data = await dffs_spGet("Owners", { select: ["Id", "Title"], filter: filter });
var titleById = {};
data.forEach(function (o) { titleById[o.Id] = o.Title; });
items.forEach(function (item) {
item.__ownerTitle = titleById[item.OwnerId] || "";
});
}
// The customizer function for the column — synchronous, as always
function vLookupShowOwner(item, field, config) {
return item.__ownerTitle === undefined ? "" : item.__ownerTitle;
}- It runs on every load and reload, including
vLookup_loadItems()and the table’s own refresh button, so resolved values cannot go stale against the rows they describe. - A synchronous function is fine. If it returns no promise it is simply called before the table draws.
- Failure never hides the table. A name that matches no function, a function that throws or rejects, or one that never finishes (after 30 seconds) is reported in the browser console and the table is drawn regardless.
- A customizer function still cannot be
async. One that returns a promise now logs a message naming it and renders an empty cell, instead of the text[object Promise]. Move the work to a prepare function.
dffs_ready()
Called when the form is fully loaded and ready. Use this as the entry point for any initialization logic that must run after the form renders. Define on window.
window.dffs_ready = function() {
dffs_doReadonly(["Title"], false);
};dffs_PreSaveAction(exit, autosave, goToEditForm)
Called before the form is saved. Receives the same arguments as dffs_triggerSave. Awaited — async is supported. Return false (or a Promise resolving to false) to cancel the save.
window.dffs_PreSaveAction = async function(exit, autosave, goToEditForm) {
if (!dffs_checkMandatory(false)) {
return false; // cancel the save
}
};dffs_PostSaveAction(exit, autosave, goToEditForm)
Called after the form is saved. Receives the same arguments passed to dffs_triggerSave / dffs_PreSaveAction. Awaited — async is supported. When defined, you handle navigation (DFFS skips its default redirect) — return false (or a Promise resolving to false) to opt out and let DFFS run its default navigation as if no function was defined.
window.dffs_PostSaveAction = function(exit, autosave, goToEditForm) {
window.location.href = "/sites/team/SitePages/ThankYou.aspx";
};dffs_PostCancelAction()
Called after the Cancel button. Awaited — async is supported. When defined, you handle navigation (DFFS skips its default close) — return false (or a Promise resolving to false) to opt out and let DFFS close the form as if no function was defined.
window.dffs_PostCancelAction = function() {
// custom cleanup / navigation
};dffs_PreDeleteAction()
Called before an item is deleted. Awaited — async is supported. Return false (or a Promise resolving to false) to cancel the delete.
window.dffs_PreDeleteAction = async function() {
return confirm("Really delete this item?");
};dffs_PostDeleteAction()
Called after an item is deleted. Awaited — async is supported. When defined, you handle navigation (DFFS skips its default redirect) — return false (or a Promise resolving to false) to opt out and let DFFS run its default navigation as if no function was defined.
window.dffs_PostDeleteAction = function() {
window.location.href = "/sites/team/Lists/Tasks";
};dffs_PrePrintAction(ctx) Print to PDF add-on
Called when Save as PDF is clicked, before anything else in the print pipeline — before the file name template is resolved, before the tab rules are run, and before the document is rendered. Awaited — async is supported. DispForm only, because Save as PDF is.
It does two jobs, separately or together:
- Sanitize values for the printed document only — write to
ctx.values, or return an object of{ internalName: value }(merged on top of it). Changes affect the PDF only; the form on screen is untouched. UsesetFieldValue()when you want to change the form itself. - Cancel the print — return
false(or aPromiseresolving tofalse). Nothing is printed and the browser’s print dialog never opens. mDFFS shows no message of its own, the same contract asdffs_PreSaveAction, so show your own —dffs_showDialogis the easiest.
ctx property | Description |
|---|---|
values | Mutable copy of the current field values keyed by internal name. Changes affect the PDF only. |
itemData | Copy of the raw list item (ID, Created, Author, …). |
settings | Copy of the effective Print to PDF settings, including anything applied via dffs_setPdfSettingsOverride. It’s a copy, so changing it has no effect. |
formType | The form type the configuration belongs to. |
mode | Always "disp". |
window.dffs_PrePrintAction = async function(ctx) {
// Block the export until the item is approved
if (ctx.values.Status !== "Approved") {
dffs_showDialog({
title: "Cannot create PDF",
body: "This request must be approved before it can be printed."
});
return false;
}
// Mask an internal note in the PDF — the form on screen keeps its value
ctx.values.InternalNotes = "[Not included in the printed version]";
};The sanitized values are used consistently throughout the document: the printed field values, the {Token}s in the cover page, header and footer, and the {Token}s in the file name template — so a masked field can’t leak through the suggested filename.
Set a value to "" or null to print the field empty; do not delete the key, as a missing key falls back to the live field value. If the function throws, the print is cancelled and the error logged — deliberately fail-closed. See the Print to PDF guide for the full treatment.
dffs_PostPrintAction(ctx) Print to PDF add-on
Called after the browser’s print/save dialog for Save as PDF has closed. Not awaited (fire-and-forget). DispForm only. ctx contains documentTitle (the resolved file name), printDate, values (what was actually printed, including any dffs_PrePrintAction changes), itemData, settings, formType and mode.
window.dffs_PostPrintAction = async function(ctx) {
await dffs_spAdd("PDF export log", {
Title: ctx.documentTitle,
Item: String(ctx.itemData.ID),
Exported: new Date().toISOString()
});
};It cannot tell a completed export from a cancelled one. The browser exposes no signal for which button the user pressed in the print dialog, so this fires whether the PDF was saved or the dialog was dismissed. It’s suitable for “a PDF export was started”, not as proof a file exists.
dffs_PostUnloadAction(calledFrom)
Called once whenever the form is left — the panel is closed, the form is replaced by another form in the same page (e.g. DispForm switching to EditForm), or you navigate away from a full-page list form. It runs exactly once per form, whichever path closes it. Not awaited (fire-and-forget). Use it to clear intervals, timers, and event listeners so they don’t keep running after the form is gone.
The optional calledFrom argument tells you why the form was left:
| Value | The form was left because… |
|---|---|
"FormPanel" | the form panel was closed / dismissed |
"FormSwitch" | it was replaced by another form in the same page (DispForm → EditForm) |
"FormBuilder" | the form was closed or navigated away from without a panel dismiss |
window.dffs_PostUnloadAction = function(calledFrom) {
clearInterval(myTimer);
// calledFrom: "FormPanel" | "FormSwitch" | "FormBuilder"
};The argument is optional — existing function() { … } cleanups keep working unchanged, and the legacy alias dffs_custom_js_cleanup receives it too. An error thrown inside this callback is caught and logged to the console rather than breaking the form teardown. Closing a vLookup child form runs only the child’s own cleanup — it no longer triggers the parent form’s dffs_PostUnloadAction.
Read-Only Data Objects
These are populated by mDFFS and available for reading in Custom JS.
dffs_FormContext
Object keyed by listUrlName. Contains context for every open form panel.
const ctx = dffs_FormContext["MyListUrlName"];
// ctx.mode — "new" | "edit" | "disp"
// ctx.itemId — current item ID (number), or null on new forms
// ctx.listId — list GUID (no surrounding braces)
// ctx.listTitle — list display name
// ctx.listUrlName — list URL name
// ctx.listFields — all list field definition objects
// ctx.serverRelativeUrl — server-relative URL of the SITE this form's list is on
// ctx.folderPath — the item's folder, defaulting to the list root
// ctx.isChildForm — true when opened inside a vLookup panel
// ctx.itemData — read-only proxy of current field values keyed by
// internal name (writes are blocked)dffs_formcontext (all lowercase) is an alias for the same object.
dffs_beforeProperties
A snapshot of the item’s raw data as it was when the form loaded — before any user changes.
const originalTitle = dffs_beforeProperties["Title"];
const wasApproved = dffs_beforeProperties["Status"] === "Approved";dffs_fieldValueHistory
Object keyed by InternalName. Each key holds an array of every value the field has been set to during the current session (oldest first).
const history = dffs_fieldValueHistory["Status"];
// e.g. ["Draft", "In Review", "Approved"]
const firstValue = history[0];dffs_ruleState
Object containing the current evaluated match state of all rules.
// { "rule_abc123": true, "rule_xyz456": false }
if (dffs_ruleState["rule_abc123"]) {
// rule currently matched (actionYes was run)
}dffs_ruleNameToId
Object that maps rule names to their rule IDs.
const ruleId = dffs_ruleNameToId["Require approval date"];
const matched = dffs_ruleState[ruleId];dffs_userProfile
The current user’s SharePoint profile object.
const email = dffs_userProfile.Email;
const displayName = dffs_userProfile.DisplayName;
const dept = dffs_userProfile.userProperties["Department"];dffs_userGroups
A lookup of the SharePoint groups the current user is a direct member of, keyed by both the group’s title and its id:
// { "Approvers": true, "12": true, "Site Members": true, "5": true }
const isApprover = dffs_userGroups["Approvers"] === true;It is not an array, so .length, .forEach and .map do not work on it — earlier versions of this page said otherwise. Use Object.keys(dffs_userGroups) if you need to list them, bearing in mind each group appears twice, once by title and once by id.
It does not include Entra ID (Azure AD) groups, so a user who is in a SharePoint group only through an Entra ID group added to it is not listed. For those, use the Entra ID group functions below.
Entra ID group membership Professional & Enterprise
From 2.5, Custom JS can check whether the current user is a member of an Entra ID (Azure AD) group — a security group or a Microsoft 365 group. Membership is transitive: a group reached through another group counts. The memberships are read from Microsoft Graph with the User.Read permission, which Microsoft normally pre-authorizes for SharePoint Framework solutions, so no tenant setup is usually needed. The lookup is made once per page load and shared with any rule condition that uses an Entra ID group. Groups are always identified by their object id, which you can copy from the group’s page in the Entra admin center.
dffs_isMemberOfEntraGroup(groupId | groupIds)
Returns a Promise<boolean>: true when the user is a member of the group, or of any group in the array. Ids are matched case-insensitively and may include braces. Returns false when membership cannot be read at all — see the warning below.
const isFinance = await dffs_isMemberOfEntraGroup("6510e196-d412-41de-a2e3-f99e8c0ffb4a");dffs_getEntraGroupMemberships(groupIds)
Returns a Promise<string[]>: the subset of the supplied group ids the user is a member of. One call for several groups.
dffs_entraGroupsAvailable()
Returns a Promise<boolean>: false when Entra ID group membership cannot be read for this user. Normally true. It can be false if an administrator has removed the permissions Microsoft pre-authorizes, in an older tenant still on Microsoft’s earlier permission model, or for an individual user blocked by a Conditional Access policy or signed in as an external guest.
dffs_entraGroups
A lookup of the Entra ID groups the user belongs to, keyed by group object id — or null:
// { "6510e196-d412-41de-a2e3-f99e8c0ffb4a": true, ... }- It is live, not a snapshot. Read
dffs_entraGroupseach time you need it rather than copying it into a variable early in your script. - It is
nulluntil something has looked membership up — a rule condition using an Entra ID group, or one of the functions above.await dffs_entraGroupsAvailable()triggers the lookup. nullmeans unknown, never “the user is in no groups”. Do not treat it as a negative.
Example — a group id kept in a settings list
Rather than hard-coding a group id in Custom JS, keep it in a list and compare against it. This assumes a list of your own named FormSettings, with the setting name in Title and the group id in a text column named Value:
let approverGroupId = null;
async function getApproverGroupId() {
// Cached: a rule condition function is called on every evaluation pass, so without this
// the settings list would be read again on every field change.
if (approverGroupId === null) {
const rows = await dffs_spGet("FormSettings", {
filter: "Title eq 'ApproverGroupId'",
top: 1
});
approverGroupId = rows.length > 0 ? rows[0].Value : "";
}
return approverGroupId;
}
window.isApprover = async function () {
const groupId = await getApproverGroupId();
if (groupId === "") {
return false;
}
return await dffs_isMemberOfEntraGroup(groupId);
};Add a rule with the trigger Run based on the results of a Custom JS function and enter isApprover. Async functions are supported — the rule engine waits for them. The membership lookup is cached for the page load, so only the first call costs a request. For a settings list on another site, pass the site URL as the third argument to dffs_spGet.
Warning — checks phrased as a negative
dffs_isMemberOfEntraGroup returns false when membership cannot be read; it has no way to say “unknown”. That is the safe answer for a positive check, but not for a negative one: a function returning true when the user is NOT in the group returns true for everyone whenever membership is unavailable — revealing whatever the rule was meant to hide. The built-in Entra ID group rule condition avoids this by skipping the rule when it cannot tell; in Custom JS the decision is yours, so check availability first:
window.isNotApprover = async function () {
if (!(await dffs_entraGroupsAvailable())) {
// Membership is unknown. Returning true here would show the content to everyone.
return false;
}
return !(await dffs_isMemberOfEntraGroup(await getApproverGroupId()));
};For a positive check no guard is needed — false is already the safe answer.
dffs_attachments
Array of the current item’s attachment file info. Updated automatically when attachments are added or removed.
// [{ FileName: "doc.pdf", ServerRelativeUrl: "/sites/..." }]
dffs_attachments.forEach(att => {
console.log(att.FileName, att.ServerRelativeUrl);
});Write-Only Data Objects
These are objects you set from Custom JS; mDFFS reads them on a polling interval.
dffs_choice_option_hidden
Set this object to hide specific options in Choice fields. mDFFS polls it every second and re-renders when it changes.
- Key:
{InternalName}_{id}— whereidis theidproperty of the field element inform_config(e.g. for a field element{ id: "abc123", t: "field", f: "Status", ... }the key is"Status_abc123"). Notfield_Status_abc123— the DOM element ID carries afield_prefix, but this object does not. Look up the field’sidfromform_configto build the key. - Value: object where each key is the choice option value to hide, set to
true
// Hide the "Rejected" option from the Status choice field
window.dffs_choice_option_hidden = {
"Status_abc123": {
"Rejected": true
}
};
// To show it again, remove the entry or set to false
window.dffs_choice_option_hidden["Status_abc123"]["Rejected"] = false;dffs_translate_field_{listUrlName}
Set this object to display translated labels for Choice and Yes/No field values, without changing what is stored.
- The global name carries the list’s URL name as a suffix —
dffs_translate_field_Casesfor a list whose URL name isCases. Read it fromdffs_FormContextasctx.listUrlName. This is the same per-list convention asdffs_triggerRule_{listUrlName}, and it is what keeps a parent form and a vLookup child form from translating each other’s fields. - Key: the field’s internal name. Value: an object mapping each stored value to the label to show.
- Display only. The stored value is untouched, so saving the form writes the value defined in list settings — not the translation.
- Applies to choice dropdowns, radio buttons, multi-choice checkboxes, and the Yes/No toggle.
- mDFFS watches the object: assigning a new one, or changing a value inside an existing one, re-renders the form.
window.dffs_translate_field_Cases = {
Status: {
Pending: "Venter",
Approved: "Godkjent",
Rejected: "Avvist"
},
IsUrgent: {
Yes: "Ja",
No: "Nei"
}
};Coming from v1.x? The v1 changelog (v1.2.29.0) documents this as a bare
dffs_translate_field. That spelling does nothing in v2 — the object is per list and the name must carry the_{listUrlName}suffix.