Skip to content

Embedded Content Blocks

Embedded Content blocks let a host page inject custom HTML/UI into a form at runtime.

What is an “Embedded Content block”?

An Embedded Content block is a placeholder field in the form that you can replace with custom HTML at runtime.

Typical use cases:

  • Render a bespoke UI component (for example: a mini widget, summary card, address or map control, or custom buttons)
  • Collect user input in a custom UI and sync it into a normal form field (so it saves/submits normally)
  • Add contextual information (read-only hints, data pulled from your host application)

Under the hood, the host page calls window.pfFormsEngine.v1.InjectHTML(...) which replaces the content of the target field’s wrapper.

Prerequisites

Warning

Any changes to the ProcessFactorial Form DOM or changes to the data must be done via the pfFormsEngine.v1 api. Any other manipulations, whether DOM or data, are explicitly not supported.

  • The form must already be embedded/loaded on the page.
  • Your page must be able to run JavaScript in the same window as the embedded form.
  • You need the fieldId of:
    • the placeholder field you will inject into, and
    • any real form fields you want to read/write via getValue / setValue.

Quick start (copy/paste example)

Paste this into the browser devtools console on a page where the form is already injected.

Note: many modern browser devtools support top-level await. If yours doesn’t, wrap the example in an async IIFE.

const fieldIdControl = '10000000-f300-0000-0000-000000000001'; // placeholder field we will inject HTML into
const fieldIdName = '10000000-9900-0000-0000-000000000001'; // normal string field we will write data to

const api = window.pfFormsEngine && window.pfFormsEngine.v1;
if (!api) {
    throw new Error('pfFormsEngine.v1 not found on window');
}

// Wait for the form to finish initialization
const ready = await api.whenReady(15000);
if (!ready) {
    console.warn('Form not ready (contentLoaded not seen)');
}

// 1) Set a value in a “real” form field
api.setValue(fieldIdName, 'Hello from pfFormsEngine.v1');

// 2) Read it back
console.log('getValue:', api.getValue(fieldIdName));

// 3) Inject HTML (button + static text + input)
const wrapperEl = api.InjectHTML(fieldIdControl, `
    <div class="card border-secondary">
        <div class="card-body p-2">
            <div class="d-flex align-items-center justify-content-between mb-2">
                <div class="fw-semibold">Injected content</div>
            </div>
            <div class="input-group input-group-sm">
                <input id="pf_input" type="text" class="form-control" value="" placeholder="Type here…" />
                <button id="pf_btn" type="button" class="btn btn-primary">Show alert</button>
            </div>
            <div class="form-text mt-1">This value will be saved as part of the form</div>
        </div>
    </div>
`);

// InjectHTML normally returns the wrapper HTMLElement.
// If it cannot find the target wrapper, it returns an object like: { injected:false, reason:'...' }
if (!wrapperEl || typeof wrapperEl.querySelector !== 'function') {
    console.warn('InjectHTML failed:', wrapperEl);
}

// Wire events on injected DOM
const inputEl = wrapperEl.querySelector('#pf_input');
const btnEl = wrapperEl.querySelector('#pf_btn');

// Button click: show an alert with the current injected input value
btnEl.onclick = function () { alert(inputEl.value); };

// Input change: sync into the form via setValue
// Tip: suppressPipelines can be useful for simple “mirror” updates.
inputEl.addEventListener('input', function () {
    api.setValue(fieldIdName, inputEl.value, null, null, { suppressPipelines: true });
});
  1. Wait for the form to be ready

    • Call await pfFormsEngine.v1.whenReady(timeoutMs).
  2. Inject HTML into the placeholder field

    • Call pfFormsEngine.v1.InjectHTML(placeholderFieldId, html).
    • Keep your injected HTML self-contained.
    • Avoid id collisions: any id="..." inside your injected HTML must be unique on the page.
  3. Wire your own event handlers

    • Injecting HTML replaces DOM nodes, so any previous handlers inside that wrapper are gone.
    • You are responsible for wiring click/input handlers for your injected elements.
  4. Sync data into real form fields

    • Use setValue(fieldId, value) to write values into the form engine so saves/submits work.
    • Use getValue(fieldId) to read form data (including display-friendly values).
  5. Set lookup fields by query (recommended pattern)

    • When you need to set a lookup/dropdown field to a value based on a property of the reference data (instead of knowing the key in advance), use setValueByQuery.
    • Example: set a "Product Type" field to the product whose productnumber is "1":
      await api.setValueByQuery('816b765d-4145-4d42-9a54-bad53ec045e5', "productnumber eq '1'");
      
    • The function resolves the entity name and field mappings from the DOM, queries the reference data, and populates both the field value and _lookupdata automatically.
    • Important limitations (see notes below): This only works with <select> dropdowns (not typeaheads), and the target field must be on the current visible page for the reference data to be loaded.
  6. Restore / cleanup when needed

    • Use RestoreHTML(fieldId) to revert the field to its previous markup.

API reference (consumer view)

All functions below are available on window.pfFormsEngine.v1.

Readiness

  • whenReady(timeoutMs = 10000) : Promise<boolean>
    • Resolves true once the form has fired its internal “ready” signal.
    • Resolves false if it times out.

Read/write form values

  • getValue(fieldId, groupCode = null, groupIndex = null)

    • Returns { value, friendlyValue, properties, fieldId, effectiveFieldId, groupCode, groupIndex }.
    • friendlyValue may come from field display metadata (for example, lookups).
  • setValue(fieldId, value, groupCode = null, groupIndex = null, options = null)

    • Writes to the form engine and updates any matching DOM input(s) when present.
    • options.triggerEvents (default true): dispatches a synthetic input event on the primary input.
    • options.suppressPipelines (default false): temporarily prevents pipeline events while updating the value.
  • setValueByQuery(fieldId, query, options = null) : Promise<object>

    • Sets a lookup field's value by matching an OData filter against reference data.
    • The function queries the reference data cache (or fetches from the API if not cached), finds the first matching record, and:
      • sets the field's value to the matched record's key,
      • populates _lookupdata with the full record so save/submit/integrate see the complete payload.
    • query is an OData filter string (e.g. "productnumber eq '1'").
    • options.entityName — reference entity name. If omitted, resolved from the DOM data-l-name attribute on the target <select>.
    • options.groupCode / options.groupIndex — for repeating sections.
    • options.triggerEvents / options.suppressPipelines — same semantics as setValue.
    • Returns a promise resolving to { handled: true, fieldId, key, displayValue, entityName, record, setResult } on success, or { handled: false, error } on failure.
    • ⚠️ Important limitations:
      • Only works with <select> dropdowns, not typeahead/autocomplete fields.
      • The target field must be on the currently visible page for its reference data to have been loaded (reference data is lazy-loaded per page).
    • Example:
      // Set a lookup field to the product where productnumber == '1'
      await api.setValueByQuery('816b765d-4145-4d42-9a54-bad53ec045e5', "productnumber eq '1'");
      

Embedded Content helpers

  • InjectHTML(fieldId, htmlContent, groupCode = null, groupIndex = null, options = null)

    • Replaces the closest .npo-field wrapper content for the target field.
    • Default behavior: refreshes conditional/visibility maps after injection.
    • options.refreshMaps (default true): set to false if you are making many DOM changes and will call refreshConditionalMaps() once at the end.
    • Returns:
      • the wrapper HTMLElement on success, or
      • an object like { injected:false, reason:'Field wrapper not found', fieldId, groupCode, groupIndex } on failure.
    • Security: htmlContent is inserted as-is. Only inject trusted HTML.
  • GetHTML(fieldId, groupCode = null, groupIndex = null)

    • Returns { found:true, html:'...' } on success, or { found:false, reason:'...' }.
  • RestoreHTML(fieldId, groupCode = null, groupIndex = null, options = null)

    • Restores the previous wrapper HTML captured by the most recent InjectHTML call.
    • options.refreshMaps (default true).
  • refreshConditionalMaps()

    • Rebuilds conditional/required/editable maps and re-evaluates visibility.
    • Useful if you injected content that impacts layout or conditional display.

Trigger configured form events (advanced)

  • triggerEvent(fieldIdOrEventId, eventIdOrNull, options)
    • Triggers configured events using the same orchestration used by UI clicks.
    • Important behavior:
      • Does not filter by EventPipeline (it will execute all events for the key unless you specify an event id/type filter).
      • By default it skips events where IsActive === false.
    • Common options:
      • pipeline (default 'OnButtonClick'): sets the pipeline context.
      • eventId: trigger a single event by id.
      • eventType: trigger only certain event types.
      • includeInactive: set true to include inactive events.

Notes, limitations, and best practices

  • Prefer storing “real” data in normal form fields via setValue.
    • Injected inputs are not automatically persisted unless you mirror them into a real field.
  • Don’t inject untrusted HTML.
  • Don't inject any script elements. Instead wire up the scripted events via the returned element when calling InjectHTML(...)
  • Avoid duplicate DOM ids in injected markup.
  • If you inject content repeatedly, consider options.refreshMaps: false and call refreshConditionalMaps() once at the end for performance.

setValueByQuery limitations

  • Reference data must be loaded first. Reference data is lazy-loaded — the form only fetches options for a dropdown when the field is rendered on the current visible page. If the target field lives on a different tab/page, the reference data has not been fetched yet and setValueByQuery will return handled: false with a "No matching reference data found" error.
  • Dropdowns only. This function works by querying the same reference data endpoint that powers <select> dropdowns. It does not work with typeahead (autocomplete) fields, which use a different lookup mechanism.
  • The filter attribute must be in Additional Display Attributes. The field you filter on (e.g. productnumber in "productnumber eq '1'") must be included in the Additional Display Attributes of the lookup configuration. Only attributes selected there are returned by the reference data API and available for filtering. If the attribute is missing from that list, the API response won't contain it and the filter will return no results.
  • Best practice: Only use setValueByQuery when the target field is a standard <select> dropdown on the currently visible page. If you need to set a value on a field that isn't on the current page, consider navigating to that page first, or use setValue with a known key value instead.

Troubleshooting

  • pfFormsEngine.v1 not found on window

    • The form scripts may not be loaded yet, or the form is not embedded on this page.
  • whenReady(...) returns false

    • The form may not have finished loading, or an error prevented initialization.
  • InjectHTML fails with “Field wrapper not found”

    • Verify you are using the correct placeholder fieldId.
    • If the field exists inside a repeating/grouped section, pass the correct groupCode and groupIndex.