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
fieldIdof:- 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 });
});
How to use Embedded Content blocks (recommended pattern)
-
Wait for the form to be ready
- Call
await pfFormsEngine.v1.whenReady(timeoutMs).
- Call
-
Inject HTML into the placeholder field
- Call
pfFormsEngine.v1.InjectHTML(placeholderFieldId, html). - Keep your injected HTML self-contained.
- Avoid
idcollisions: anyid="..."inside your injected HTML must be unique on the page.
- Call
-
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.
-
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).
- Use
-
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
productnumberis"1": - The function resolves the entity name and field mappings from the DOM, queries the reference data, and populates both the field value and
_lookupdataautomatically. - 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.
- 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
-
Restore / cleanup when needed
- Use
RestoreHTML(fieldId)to revert the field to its previous markup.
- Use
API reference (consumer view)
All functions below are available on window.pfFormsEngine.v1.
Readiness
whenReady(timeoutMs = 10000) : Promise<boolean>- Resolves
trueonce the form has fired its internal “ready” signal. - Resolves
falseif it times out.
- Resolves
Read/write form values
-
getValue(fieldId, groupCode = null, groupIndex = null)- Returns
{ value, friendlyValue, properties, fieldId, effectiveFieldId, groupCode, groupIndex }. friendlyValuemay come from field display metadata (for example, lookups).
- Returns
-
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(defaulttrue): dispatches a syntheticinputevent on the primary input.options.suppressPipelines(defaultfalse): 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
_lookupdatawith the full record so save/submit/integrate see the complete payload.
queryis an OData filter string (e.g."productnumber eq '1'").options.entityName— reference entity name. If omitted, resolved from the DOMdata-l-nameattribute on the target<select>.options.groupCode/options.groupIndex— for repeating sections.options.triggerEvents/options.suppressPipelines— same semantics assetValue.- 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).
- Only works with
- Example:
Embedded Content helpers
-
InjectHTML(fieldId, htmlContent, groupCode = null, groupIndex = null, options = null)- Replaces the closest
.npo-fieldwrapper content for the target field. - Default behavior: refreshes conditional/visibility maps after injection.
options.refreshMaps(defaulttrue): set tofalseif you are making many DOM changes and will callrefreshConditionalMaps()once at the end.- Returns:
- the wrapper
HTMLElementon success, or - an object like
{ injected:false, reason:'Field wrapper not found', fieldId, groupCode, groupIndex }on failure.
- the wrapper
- Security:
htmlContentis inserted as-is. Only inject trusted HTML.
- Replaces the closest
-
GetHTML(fieldId, groupCode = null, groupIndex = null)- Returns
{ found:true, html:'...' }on success, or{ found:false, reason:'...' }.
- Returns
-
RestoreHTML(fieldId, groupCode = null, groupIndex = null, options = null)- Restores the previous wrapper HTML captured by the most recent
InjectHTMLcall. options.refreshMaps(defaulttrue).
- Restores the previous wrapper HTML captured by the most recent
-
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.
- Does not filter by
- Common options:
pipeline(default'OnButtonClick'): sets the pipeline context.eventId: trigger a single event by id.eventType: trigger only certain event types.includeInactive: settrueto 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: falseand callrefreshConditionalMaps()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
setValueByQuerywill returnhandled: falsewith 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.
productnumberin"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
setValueByQuerywhen 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 usesetValuewith 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(...)returnsfalse- The form may not have finished loading, or an error prevented initialization.
-
InjectHTMLfails 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
groupCodeandgroupIndex.
- Verify you are using the correct placeholder