Front end

JavaScript

The browser-side API: the per-element interfaces Forms and Popups attach, the events they fire, and the editor filters they run through @wordpress/hooks.

XPAC exposes two different JavaScript surfaces, and it is worth knowing which one you need before you write anything.

SurfaceWhere it runsWhat it is
Filters through @wordpress/hooksMostly the block editoraddFilter() on a named hook, the same API WordPress core uses
Element interfacesThe front endAn object each plugin attaches to the DOM element it controls

The first is listed in full in the hook reference — every JavaScript hook is generated from its applyFilters() call site, with its arguments. This page covers the second, which is not generated because it is not a hook: it is an object on an element, and it is the only way to drive a form or a popup from outside.

Getting your script on the page

Enqueue against the handle you want to run after. The handles are stable:

HandleWhat it is
xp-form-view-scriptThe Forms front-end bundle. Attaches form.xpacForm
xp-form-editor-scriptThe Form block's editor script
xpac-form-settingsThe form editor sidebar, where the Forms editor filters run
xpac-popupThe Popups front-end bundle. Attaches template.xpacPopup
add_action('wp_enqueue_scripts', function () {
	wp_enqueue_script(
		'my-form-extras',
		plugins_url('my-form-extras.js', __FILE__),
		['xp-form-view-script'],
		'1.0.0',
		true
	);
});

The Forms bundle is enqueued by the block, so it is only on pages that render a form. A dependency on xp-form-view-script means your script is only on those pages too, which is usually what you want — but it also means a page with no form never loads it.

Forms

The lifecycle

A form is initialised once. The interface is attached first, then two events announce it, in this order:

EventFired onDetailWhen
initializethe <form> element—form.xpacForm has just been attached
xpacFormInitializedocument{ form }Immediately after, for listeners that were not bound to the element
xpacFormsReadydocument—Once per page, after every form present at load has been initialised

Listen on document unless you already hold the element. A form inserted later — by a popup, a router, an AJAX load — fires xpacFormInitialize too, and never fires xpacFormsReady a second time.

document.addEventListener('xpacFormInitialize', (event) => {
	const form = event.detail.form;

	form.xpacForm.callbacks.registerSuccessCallback(() => {
		console.log('submitted', form.xpacForm.utils.getId());
	});
});

To initialise a form the bundle has not seen — one you built and inserted yourself — call window.initializeXpacForm(formElement). It is a no-op on an element that is not .xp-block-form or that already has an interface.

form.xpacForm

Five namespaces. Everything a submission does can be observed or changed through them.

config

config.setOption(name, value) and config.getOption(name, fallback) hold per-form settings. setOption will not overwrite a name that is already set, so the first writer wins.

window.xpacForms

Read-only data printed once per page, before the form bundle, the first time a form renders. It is deep-frozen, so nothing can change it in the browser.

KeyContents
nonceA wp_rest nonce for the submission request
healthCheckUrlThe endpoint the bundle calls to refresh an expired nonce
validationMessagesThe site's validation message strings, including phoneNumber and phoneNumberCountry for Smart international phone fields
submissionMessagesThe site's submission message strings
maskScriptThe address of the IMask library (xpac-form-mask), loaded by registerMask only when a mask needs it
registeredBlocksConfigThe field blocks registered on the site
phoneFieldPhone field data: masks (phone formats added through xpac_forms_phone_masks), siteCountry (the site's two-letter country, or '') and i18n (the country picker's labels). Present only on a page that renders a Smart international field or a field with an add-on phone format

Add-ons put their own front-end data here through the xpac_forms_inline_js_data filter — Turnstile and reCAPTCHA their site keys, Stripe its publishable key. The keys above are merged in afterwards and cannot be replaced.

Popups

template.xpacPopup

Every popup template element carries an interface. It is attached before the element's initialize event fires, which is the point to register a checker.

MethodWhat it does
getSettings()The popup's saved settings, including its rules array
getState(){ open, activeRule, closedByClick }
setState(partial)Merge into that state
registerOpenChecker(cb)Veto an open. Return false to stop it
registerHideChecker(cb)Veto a close the same way

Both checkers receive (rule, template). Every registered open checker has to return truthy for the popup to open — one false is enough to hold it back.

document.addEventListener('initialize', (event) => {
	const template = event.target;

	if (!template.xpacPopup) {
		return;
	}

	template.xpacPopup.registerOpenChecker(
		() => !document.body.classList.contains('checkout-in-progress')
	);
}, true);

The listener is capturing because initialize is dispatched on the template element and does not bubble.

Events

Four events, each fired twice: once on the template element and once on document.

EventWhenDetail
xpac-popup-pre-openBefore the popup is built and shown{ rule }
xpac-popup-post-openAfter it is shown and focused{ rule, requested }
xpac-popup-pre-closeBefore it is hidden{ rule }
xpac-popup-post-closeAfter it is hidden{ rule }

The document copy adds template to the detail, since a document listener has no other way to tell which popup it is hearing about. requested is true when the open came from an explicit request — a Popup Selector block or a trigger — rather than from a display rule matching.

Listen on document for anything site-wide, and on the element when you already have it. Do not listen for both, or you will handle every popup twice.

Editor filters

Both plugins run their editor UI through @wordpress/hooks, so an add-on adds a panel or changes a field without touching the plugin.

FilterWhat it changes
xpac-forms-form-panelsThe panels in the form's document sidebar
xpac-forms-field-panelsThe panels shown when a field block is selected
xpac-forms-field-output-propsThe props a field renders with
xpac-forms-field-nameThe generated name attribute of a new field
xpac-forms-after-settings-modal-contentContent appended inside a settings modal
xpac-popups-popup-panelsThe panels in the popup sidebar
xpac-popups-types / xpac-popups-type-labelsThe popup types and their labels
xpac-popup-display-rule-additional-componentsExtra controls in the display-rule editor
xpac-popups-display-rule-default-configThe defaults a new display rule starts from

Five more run on the front end rather than in the editor: xpac-popups-triggers, xpac-popups-animation, xpac-popups-is-modal, xpac-popups-is-conversion and xpac-popups-conversion-selector.

Each one's arguments, defaults and call site are in the hook reference.

import { addFilter } from '@wordpress/hooks';

addFilter('xpac-forms-form-panels', 'my-plugin/panels', (panels) => [
	...panels,
	{ name: 'my-panel', title: 'My panel', Component: MyPanel },
]);

On this page