← Documentation

Custom Render Function — Guide

Professional & Enterprise

Replace a text field’s input control with HTML you generate in JavaScript — a picker from another list, a chip selector, a slider, a card layout — while rules, validation, required fields and save all keep working exactly as before.

When to reach for this

Try the built-in field enhancements first — dropdown and autocomplete from a SharePoint list or REST endpoint, barcode, GPS, signature and star rating are all configuration, no code. Use a custom render function when you need a control that none of those provide.

You will also want the Custom JS — Window API Reference open alongside this page: the helpers listed below are documented there in full.

1. Which field types support it

The setting appears only on:

  • Single line of text
  • Multiple lines of plain text (Rich text must be off — see Rich Text Fields)

No other field type offers it. On those two field types it is mutually exclusive with the other field modes — enabling it automatically turns off Field pattern, GPS capture, Barcode scanner, Render as dropdown, Autocomplete and Signature capture.

2. Setting it up (form builder)

  1. Add the text field to the form and open its field properties panel.
  2. Scroll to Custom render function and toggle it on.
  3. Enter the name of your global function — just the name, e.g. dffs_categoryPicker (no window. prefix, no parentheses).
  4. Save the configuration.
  5. Define the function itself in Custom JS or in a file referenced by Misc → Load external JS, so that it exists on window before the form renders.

If the named function isn’t found at render time, the field shows an error message in place of the control. The same happens if your function throws.

3. The function contract

window.yourFunctionName = function (field, isReadonly, listContext) {
    return "<div>…your HTML…</div>";   // or a Promise that resolves to an HTML string
};

3.1 Parameters

ParameterDescription
fieldThe SharePoint field object for this column — InternalName, Title, MaxLength, Required, Description, etc.
isReadonlytrue on the display form, or when a rule has set the field read-only. Your function is called in both cases — branch on this to render a read-only view.
listContext{ mode, itemId, listId, listTitle, listUrlName, serverRelativeUrl, isChildForm, panelId } — the current form’s context. mode is the DFFS mode, isChildForm is true when the form is open inside a vLookup child panel.

3.2 Return value

  • A string — inserted as HTML into the field’s form-body area.
  • A Promise resolving to a string — a spinner is shown in the field until it resolves. This is what you use when you need to query SharePoint first.

The field’s label, tooltip and description still render normally — you’re only replacing the input control itself.

4. Four rules you must know

These follow from how the HTML is injected, and cause most of the confusion when people first use the feature.

4.1 The result is cached — your function runs once

The output is cached per field + function name, so the function is not re-run on every form re-render. This is deliberate: it means calling setFieldValue() from inside your control won’t wipe out the DOM the user is interacting with.

To force a re-run (e.g. a cascading dropdown whose parent field just changed):

dffs_refreshCustomRender("YourFieldInternalName");  // clears one field
dffs_refreshCustomRender();                          // clears all custom-rendered fields

4.2 Neither <script> tags nor inline handlers will run

The HTML is injected via innerHTML, so any <script> block inside it is inert.

Inline handler attributes do not work either. onclick="…" and onchange="…" are blocked by the Content Security Policy that Microsoft began enforcing for SharePoint Online on 1 March 2026. Older examples — including earlier versions of this page — used them, and they fail silently: the control renders, but nothing happens when you interact with it.

Attach listeners after the HTML reaches the DOM instead:

  1. Give the elements you need to reach an id, or mark them with a data- attribute.
  2. End your render function with setTimeout(fn, 0). The string you return is not in the DOM yet, so the lookup has to wait a tick.
  3. Inside that callback, find the element and call addEventListener.
// Unique per render, so re-renders and vLookup child panels cannot collide
window.__dffsUid = (window.__dffsUid || 0) + 1;
var selId = "dffs_" + field.InternalName + "_" + window.__dffsUid;

setTimeout(function () {
    var el = document.getElementById(selId);
    if (!el) { return; }
    el.addEventListener("change", function () {
        setFieldValue(field.InternalName, el.value);
    });
}, 0);

Repainting your own markup? Bind one delegated listener to the container rather than one per child. A listener on the container survives replacing its innerHTML; listeners on the children do not.

4.3 Typing into your control saves nothing by itself

The real field control is gone, so the browser has no idea your <select> or <div> relates to a SharePoint column. You must call setFieldValue() — that is what feeds the form’s value store, rules, validation and save:

setFieldValue(field.InternalName, newValue);

4.4 The form builder preview shows a placeholder

Inside the configuration/preview canvas the field shows a placeholder string instead of running your code. Always test on a real form.

5. Helper functions available to you

All of these are global and are automatically routed to the currently active form panel, so they behave correctly even inside vLookup child forms.

FunctionPurpose
getFieldValue(fin[, property])Read the current value of any field on the form.
setFieldValue(fin, value)Write a value — this is what gets saved.
dffs_refreshCustomRender([fin])Clear the cache and re-run the custom render function(s).
dffs_spGet(list, options[, siteUrl])Query a list. options: { select, expand, filter, orderBy, orderAscending, top }.
dffs_spGetById(list, itemId[, options][, siteUrl])Fetch one item.
dffs_spAdd(list, data[, siteUrl])Create an item.
dffs_spUpdate(list, itemId, data[, siteUrl])Update an item.
dffs_spDelete(list, itemId[, siteUrl])Recycle an item.
dffs_showModal(...) / dffs_hideModal(id)Open/close a DFFS modal — handy for a full-screen picker launched from a button in your rendered HTML.
dffs_doReadonly(fin) / dffs_undoReadonly(fin)Toggle a field’s read-only state.

list may be a list title or a list GUID. The optional trailing siteUrl lets you read from another site or site collection.

6. Example — dropdown populated from another list

A single line of text field rendered as a dropdown whose options come from a Categories list.

function dffs_esc(s) {
    return String(s === null || s === undefined ? "" : s)
        .replace(/&/g, "&amp;").replace(/</g, "&lt;")
        .replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}

window.dffs_categoryPicker = async function (field, isReadonly, ctx) {
    var fin = field.InternalName;
    var current = getFieldValue(fin) || "";

    // Display form / read-only: just show the stored value
    if (isReadonly) {
        return "<div class='dffs-field-value'>" + dffs_esc(current) + "</div>";
    }

    // Query another list in the same site.
    // Add a 3rd argument (siteUrl) to read from a different site collection.
    var items = await dffs_spGet("Categories", {
        select: ["Id", "Title", "SortOrder"],
        filter: "Active eq 1",
        orderBy: "SortOrder",
        orderAscending: true,
        top: 500
    });

    window.__dffsUid = (window.__dffsUid || 0) + 1;
    var selId = "dffs_cat_" + fin + "_" + window.__dffsUid;

    // Inline onchange="" is blocked by CSP — bind the listener once the
    // markup is in the DOM instead.
    setTimeout(function () {
        var el = document.getElementById(selId);
        if (!el) { return; }
        el.addEventListener("change", function () {
            setFieldValue(fin, el.value);
        });
    }, 0);

    var html = "<select id='" + selId + "' style='width:100%;padding:6px;'>";
    html += "<option value=''>-- select --</option>";

    items.forEach(function (it) {
        var sel = it.Title === current ? " selected" : "";
        html += "<option value='" + dffs_esc(it.Title) + "'" + sel + ">" +
                dffs_esc(it.Title) + "</option>";
    });

    return html + "</select>";
};

Cascading variant. If the options should depend on another field, read the parent inside the function and re-run it when the parent changes:

window.dffs_subCategoryPicker = async function (field, isReadonly, ctx) {
    var parent = getFieldValue("Category");
    if (!parent) {
        return "<div class='dffs-field-value'><em>Select a category first</em></div>";
    }
    var items = await dffs_spGet("SubCategories", {
        select: ["Title"],
        filter: "Category eq '" + parent.replace(/'/g, "''") + "'",
        orderBy: "Title"
    });
    // …build the <select> exactly as above…
};

Then, from a rule that triggers when the Category field changes:

dffs_refreshCustomRender("SubCategory");

7. Example — a genuine custom control (multi-select chips)

Renders the options as clickable “chips” and stores the selection as a semicolon-separated string in the text field. This shows the pattern for a control that manages its own DOM between form re-renders.

window.dffs_chipPicker = async function (field, isReadonly, ctx) {
    var fin = field.InternalName;
    var selected = (getFieldValue(fin) || "").split(";").filter(Boolean);

    var options = await dffs_spGet("Skills", { select: ["Title"], orderBy: "Title", top: 500 });

    if (isReadonly) {
        return selected.map(function (v) {
            return "<span class='dffs-chip'>" + dffs_esc(v) + "</span>";
        }).join(" ");
    }

    // One id per render, and the state bucket hangs off the same key. Keying on
    // the field name alone breaks as soon as the field renders twice — after a
    // dffs_refreshCustomRender, or inside a vLookup child panel — because both
    // copies would answer to the same id and share one selection.
    window.__dffsUid = (window.__dffsUid || 0) + 1;
    var uid = "chips_" + fin + "_" + window.__dffsUid;

    window.__chipState = window.__chipState || {};
    window.__chipState[uid] = {
        fin: fin,
        selected: selected,
        options: options.map(function (o) { return o.Title; })
    };

    // One delegated listener on the wrapper. The chips are repainted on every
    // toggle, so a listener bound to an individual chip would not survive.
    setTimeout(function () {
        var wrap = document.getElementById(uid);
        if (!wrap) { return; }
        wrap.addEventListener("click", function (e) {
            var chip = e.target.closest("[data-chip]");
            if (chip) { dffs_chipToggle(uid, chip.getAttribute("data-chip")); }
        });
    }, 0);

    return "<div id='" + uid + "'>" + dffs_chipMarkup(uid) + "</div>";
};

window.dffs_chipMarkup = function (uid) {
    var st = window.__chipState[uid];
    return st.options.map(function (opt) {
        var on = st.selected.indexOf(opt) > -1;
        return "<span data-chip='" + dffs_esc(opt) + "' " +
            "style='display:inline-block;margin:2px;padding:4px 10px;border-radius:12px;" +
            "cursor:pointer;border:1px solid #8a8886;" +
            (on ? "background:#107c10;color:#fff;" : "background:#fff;color:#323130;") + "'>" +
            dffs_esc(opt) + "</span>";
    }).join("");
};

window.dffs_chipToggle = function (uid, value) {
    var st = window.__chipState[uid];
    var i = st.selected.indexOf(value);
    if (i > -1) { st.selected.splice(i, 1); } else { st.selected.push(value); }

    // 1. Persist to the form field — this is what actually gets saved
    setFieldValue(st.fin, st.selected.join(";"));

    // 2. Repaint our own markup — the render function is cached and will NOT re-run
    document.getElementById(uid).innerHTML = dffs_chipMarkup(uid);
};

8. Worked example — an asset picker on a Helpdesk form

Goal: On a Helpdesk tickets form, let the user pick an asset from an Assets list, showing the asset’s model and location alongside the name, and store the asset tag in a text column.

  1. On the Tickets list, create a single line of text column AssetTag.
  2. In the form builder, add AssetTag to the form, open its properties, enable Custom render function and enter dffs_assetPicker.
  3. In Custom JS:
    window.dffs_assetPicker = async function (field, isReadonly, ctx) {
        var fin = field.InternalName;
        var current = getFieldValue(fin) || "";
    
        var assets = await dffs_spGet("Assets", {
            select: ["Title", "AssetTag", "Model", "Location"],
            filter: "Status eq 'In service'",
            orderBy: "Title",
            top: 500
        });
    
        if (isReadonly) {
            var hit = assets.filter(function (a) { return a.AssetTag === current; })[0];
            return hit
                ? "<div><strong>" + dffs_esc(hit.Title) + "</strong><br>" +
                  "<span style='color:#605e5c;font-size:12px;'>" +
                  dffs_esc(hit.Model) + " — " + dffs_esc(hit.Location) + "</span></div>"
                : "<div class='dffs-field-value'>" + dffs_esc(current) + "</div>";
        }
    
        window.__dffsUid = (window.__dffsUid || 0) + 1;
        var selId = "dffs_asset_" + fin + "_" + window.__dffsUid;
    
        setTimeout(function () {
            var el = document.getElementById(selId);
            if (!el) { return; }
            el.addEventListener("change", function () {
                setFieldValue(fin, el.value);
            });
        }, 0);
    
        var html = "<select id='" + selId + "' style='width:100%;padding:6px;'>" +
                   "<option value=''>-- select an asset --</option>";
        assets.forEach(function (a) {
            html += "<option value='" + dffs_esc(a.AssetTag) + "'" +
                    (a.AssetTag === current ? " selected" : "") + ">" +
                    dffs_esc(a.Title) + " (" + dffs_esc(a.Model) + ", " + dffs_esc(a.Location) + ")" +
                    "</option>";
        });
        return html + "</select>";
    };
  4. Save and open a ticket form. The user picks from a live, filtered list of assets; the asset tag is what gets stored, and the display form shows a formatted summary instead of a bare code.

9. Notes and limits

  • No inline handlers. onclick / onchange attributes are blocked by the Content Security Policy enforced for SharePoint Online since 1 March 2026. Use addEventListener from a setTimeout(fn, 0) as shown in 4.2 — and prefer one delegated listener on a container over one per element.
  • Security. Your HTML is injected as-is. Always escape values coming from list data (see the dffs_esc helper used in the examples) so a list item can’t inject markup into the form.
  • Async errors. If your Promise rejects, the field shows an error message with the function name — check the browser console for the underlying cause.
  • Print to PDF. For custom-rendered fields the PDF export reads the live DOM rather than re-running your function, so whatever is on screen is what appears in the printout.
  • License downgrade. Custom render function is a Professional-tier field feature. If a form configured under a higher tier is opened on a lower-tier license, the setting is stripped at runtime and the field falls back to its normal control.
  • Keep functions global and idempotent. Define them once (Custom JS or an external JS file); avoid work that must not happen twice, since dffs_refreshCustomRender can re-invoke them.
  • One function per purpose, reused across fields. The field argument tells you which column you’re rendering, so a single function can serve several fields — branch on field.InternalName.