Facet Events API
Facet events are browser CustomEvent objects for observing facet controls, customizing authored result markup, and reacting to completed result updates. Listen on a component root when you own one instance, or use event delegation on document when several facets share an integration.
The events are documented as observation contracts, not as a command API. Do not dispatch synthetic copies to operate a facet. For backward compatibility, some component change event names are also consumed by OME's internal adapters, so synthetic dispatch can trigger legacy internal behavior; that behavior is unsupported and must not be used as an integration contract. Propagation methods are not a control API either.
Which Event Should I Use?
| Goal | Event |
|---|---|
| Observe a Select opening or closing. | ome:select:open or ome:select:close |
| Observe a Search Select opening, closing, or receiving query text. | ome:search-select:open, ome:search-select:close, or ome:search-select:input |
| React to a value committed directly through one control. | The component-specific change event, ome:map-address-change, ome:map-radius-change, or ome:map:bbox-change |
| Release resources that belong to result content about to be removed. | ome:facet:target-will-update |
| Change links, text, classes, attributes, or widgets in newly inserted authored results. | ome:facet:target-content-updated |
| Run work after the complete synchronous target update has settled. | ome:facet:target-updated |
| Observe count, pagination, or Map POI response metadata. | ome:facet:response |
| Observe creation of a Map Facet instance. | ome:map:initialized |
Use the Facet Target lifecycle for integrations that care about result changes from any cause, including Reset Facet, pagination, Load More, and URL-driven requests. Control events describe the control itself; they do not guarantee that a request or target update follows.
Shared Event Rules
Public events dispatched from a facet or Map component root use this DOM configuration:
{
bubbles: true,
composed: true,
cancelable: false,
}
This means:
event.targetis the component root or physical Facet Target that emitted the event.- A listener on
documentcan use event delegation across component instances. - Calling
preventDefault()cannot cancel OME behavior. - Do not use
stopPropagation()orstopImmediatePropagation()to control OME. An intentionally earlierstopImmediatePropagation()can prevent other observers from receiving a DOM event and is unsupported. - Event handlers run synchronously. Returning a promise or using
awaitdoes not pause OME.
ome:facet:response is the exception to the root dispatch rule: it is dispatched directly on document. It remains composed and non-cancelable, but bubbling has no practical extra reach from that target.
Stable semantic fields
New public fields use snake_case and contain serializable semantic data. Control events share:
interface FacetControlDetail {
readonly target_id: string;
readonly facet_id: string;
}
target_ididentifies the logical result scope that the control affects.facet_ididentifies the filter or query-clause key, such ascategory_name,geo_radius, orgeo_bbox.event.targetidentifies the exact component instance. You do not need a DOM reference insideevent.detail.- A cleared scalar value is
nullin the stable fields.
Existing camelCase fields and DOM-element references are retained as legacy compatibility fields. They are documented separately below and are not used in new examples.
Interaction Events
Interaction events report transient UI state. They do not mean that the selected value or result content changed.
| Event | Dispatch target | When it fires | Stable detail |
|---|---|---|---|
ome:select:open | Select Facet root. | Its popup opens. | Control fields, selection snapshot, mode, state: "open", and reason. |
ome:select:close | Select Facet root. | Its popup closes. | Control fields, selection snapshot, mode, state: "closed", and reason. |
ome:search-select:open | Search Select Facet root. | Its popup opens. | Select fields plus query. |
ome:search-select:close | Search Select Facet root. | Its popup closes. | Select fields plus query. |
ome:search-select:input | Search Select Facet root. | Its internal search query changes. | Control fields, selection snapshot, mode, query, and previous_query. |
Select and Search Select open/close reasons are:
type FacetInteractionReason =
| "trigger"
| "item-select"
| "escape"
| "outside-click"
| "tab"
| "another-control"
| "input-focus"
| "input-change";
Only Search Select uses input-focus and input-change. another-control means that another overlay handled by the same choice runtime opened: another Select-style UIChoice for Select, or another Search Select for Search Select. Keep a fallback branch when switching on reason, because future versions can add another semantic reason.
Value Change Events
A value change event reports a value committed directly through that control by the user.
These events are not emitted for initial HTML or URL hydration, option-availability synchronization, dependent Map synchronization, or as one event per control during a global Reset Facet action. Observe the Facet Target lifecycle when the integration cares about the resulting content rather than the direct control interaction.
| Event | Dispatch target | Stable detail |
|---|---|---|
ome:select:change | Select Facet root. | Selection snapshot and mode. |
ome:search-select:change | Search Select Facet root. | Selection snapshot, mode, and query. |
ome:checkbox-list:change | Checkbox List Facet root. | selected_values, previous_values, changed_value, and checked. |
ome:radio-list:change | Radio List Facet root. | Selection snapshot. |
ome:map-address-change | Map Address Facet root. | Nested address snapshot or null. |
ome:map-radius-change | Map Radius Facet root. | Nested radius snapshot or null. |
ome:map:bbox-change | Map Facet root. | Numeric bounds, center, and zoom. |
Select, Search Select, and Radio snapshots
Selection events use a common semantic snapshot:
interface FacetSelectionSnapshot {
readonly selected_value: string | null;
readonly previous_value: string | null;
readonly selected_values: readonly string[];
readonly previous_values: readonly string[];
}
type FacetSelectMode = "single" | "multiple";
Select and Search Select add mode. Search Select also includes its current query. Radio List exposes the arrays as well as the nullable scalar values, so one delegated selection handler can work across all three component families.
Checkbox List payload
interface CheckboxListChangeDetail extends FacetControlDetail {
readonly selected_values: readonly string[];
readonly previous_values: readonly string[];
readonly changed_value: string;
readonly checked: boolean;
}
changed_value and checked describe the directly toggled item. The two arrays describe the complete state before and after that action.
Map Address payload
interface MapAddressChangeDetail extends FacetControlDetail {
readonly facet_id: "geo_radius";
readonly address: {
readonly label: string;
readonly lat: number;
readonly lng: number;
readonly provider_id: string;
} | null;
}
Selecting a suggestion supplies the human-readable label and numeric coordinates. Clearing that selection supplies address: null. Map Address and Map Radius share facet_id: "geo_radius" because together they form one proximity clause.
Map Radius payload
interface MapRadiusChangeDetail extends FacetControlDetail {
readonly facet_id: "geo_radius";
readonly radius: {
readonly value: number;
readonly unit: "mi" | "km";
} | null;
}
Map bounding-box payload
interface MapBboxChangeDetail extends FacetControlDetail {
readonly facet_id: "geo_bbox";
readonly bounds: {
readonly south: number;
readonly west: number;
readonly north: number;
readonly east: number;
};
readonly bbox: string;
readonly center: {
readonly lat: number;
readonly lng: number;
};
readonly zoom: number;
}
The stable values are plain serializable values. bbox is the serialized south,west,north,east clause value; bounds avoids requiring consumers to parse it. The event does not expose a Leaflet bounds or Map instance.
Facet Target Update Lifecycle
The lifecycle describes accepted dynamic content updates, not the request lifecycle:
old authored content is present
│
├─ ome:facet:target-will-update
│
├─ replace or append content and reconcile fallback
│
├─ ome:facet:target-content-updated ← synchronous customization seam
│
├─ OME hydration, response metadata, dependent Map sync,
│ returned script scheduling, URL sync, and loading cleanup
│
└─ ome:facet:target-updated
All three events have the same detail:
interface FacetTargetLifecycleDetail {
readonly target_id: string;
readonly template_id: string;
readonly update_mode: "replace" | "append";
}
| Event | DOM visible to the listener | Intended use |
|---|---|---|
ome:facet:target-will-update | Existing result content is still present. | Release third-party observers, widgets, or resources owned by the outgoing authored content. |
ome:facet:target-content-updated | Replacement or appended content and fallback state are already in the physical target. Dependent result views have not consumed that content yet. | Synchronously customize newly inserted authored result markup. This is the only supported mutation seam. |
ome:facet:target-updated | Synchronous OME target work is complete and the loading state is cleared. | Observe or measure the settled update. |
update_mode: "replace" means the response replaced the result content. update_mode: "append" means Load More or infinite scroll extended the existing result content. A content handler must be idempotent because it can run repeatedly against appended or replacement markup.
Physical targets and logical targets
Several Facet Target elements can share one target_id. The lifecycle emits one event per physical Facet Target for each phase, with that element available as event.target. The response event is different: ome:facet:response emits once per logical target_id update. When those physical targets use different templates, its summary can combine fields from more than one template response; the precedence rules are documented below.
Lifecycle limits
- A failed, invalid, aborted, or stale response does not begin the content lifecycle.
- A rare synchronous failure in a later OME hydration step can happen after
will-updateandcontent-updated. OME then restores the previous content and omitstarget-updated. There is no rollback event in v1, so customization handlers must be idempotent and release old integrations again on the nextwill-update. - The initial server render does not emit these events. Run an enhancement function once for existing markup, then listen for dynamic
ome:facet:target-content-updatedevents. - These are content events, not a request lifecycle. They do not expose request start, network timing, request data, a correlation identifier, or cancellation.
- There is no rollback phase in v1. A future additive detail field can identify another update cause without changing
update_mode. - Handlers are synchronous and non-cancelable. OME does not wait for promises returned by a listener.
ome:facet:target-updatedconfirms synchronous hydration and scheduling. It does not wait for returned module scripts or other async modules to finish executing.
Supported Customization Boundary
During ome:facet:target-content-updated, you may synchronously change authored result markup that your site owns:
- update an authored link
hrefor imagesrc; - change authored text;
- add or remove your own classes and attributes;
- initialize your own result widget or add your own authored element.
The following are unsupported:
- changing or removing OME component roots or OME-owned
data-ome-*attributes; - changing Facet Target identity, Map POI identity, or Map coordinates;
- mutating
event.detail, the response summary,pois, orselected_content; - treating
will-updateortarget-updatedas markup mutation hooks; - delaying OME with async work or trying to cancel the lifecycle.
Use your own class or data attribute as the integration selector. This keeps the handler independent of OME runtime markup.
Facet Response Event
ome:facet:response is an advanced, read-only summary dispatched on document once per logical accepted target update.
interface FacetResponseDetail {
readonly target_id: string;
readonly count: number;
readonly pagination?: {
readonly per_page: number;
readonly offset: number;
readonly current_page: number;
readonly total_pages: number;
readonly total_items: number;
readonly has_prev_page: boolean;
readonly has_next_page: boolean;
readonly range_start: number;
readonly range_end: number;
readonly unbounded?: boolean;
};
readonly pois?: readonly {
readonly value: string;
readonly slug?: string;
readonly lat: number;
readonly lng: number;
readonly hover_content?: string;
readonly selected_content?: string;
}[];
readonly visible_values?: readonly string[];
readonly map_poi_detail_mode?: "full" | "lightweight";
readonly map_poi_detail_threshold?: number;
readonly map_poi_result_count?: number;
readonly map_poi_truncated?: boolean;
}
For a logical target backed by multiple template responses, OME builds the summary in registered target order:
countand other base fields come from the first registered Facet Target template.paginationcomes from the last template response in that order that contains pagination metadata.- Map fields come from the first template response in that order that contains any Map payload.
The resulting fields can therefore describe different physical templates that share the same target_id. Treat the event as a logical-target summary rather than as one raw response. Pagination fields are absent when none of the template responses includes pagination metadata. Map fields are absent when none includes Map data.
The event fires after every physical target has received its new content, availability and pagination metadata have been applied, and the internal Map synchronization has run. It fires before ome:facet:target-updated, which additionally confirms returned-script scheduling, URL synchronization, and loading cleanup.
Use this event for analytics, diagnostics, or read-only synchronization. Do not mutate its detail to change results or Map POIs. Internal Map synchronization does not depend on listener order or on consumers modifying this public event; use ome:facet:target-content-updated for authored markup customization.
Map Initialized Event
ome:map:initialized is dispatched from the Map Facet root after its runtime instance and initial POI state have been created.
interface MapInitializedDetail {
readonly target_id: string;
}
It does not mean that map tiles or animations have finished, and it does not expose a Leaflet instance or a public Map control API.
Example: Handle Selection Facets Together
const selection_events = [
"ome:select:change",
"ome:search-select:change",
"ome:radio-list:change",
];
for (const event_name of selection_events) {
document.addEventListener(event_name, (event) => {
if (!(event instanceof CustomEvent)) {
return;
}
const { target_id, facet_id, selected_values } = event.detail;
if (target_id === "products" && facet_id === "category_name") {
console.log("Selected categories", selected_values);
}
});
}
Example: Observe A Settled Target Update
document.addEventListener("ome:facet:target-updated", (event) => {
if (
!(event instanceof CustomEvent) ||
!(event.target instanceof HTMLElement)
) {
return;
}
if (event.detail.target_id !== "articles") {
return;
}
console.log(
"Rendered article cards",
event.target.querySelectorAll(".article-card").length,
);
});
Example: Add saddr To Map POI Direction Links
Suppose the selected slot of each authored Map POI contains this link:
<a
data-directions-link
href="https://maps.google.com/?daddr={item.meta.latitude},{item.meta.longitude}"
>
Get directions
</a>
Keep the selected address by logical target, then change the authored links only in the content-update seam:
const start_addresses = new Map();
function update_direction_links(root, target_id) {
const start_address = start_addresses.get(target_id) ?? null;
for (const link of root.querySelectorAll("[data-directions-link]")) {
if (!(link instanceof HTMLAnchorElement)) {
continue;
}
const authored_href = link.getAttribute("href");
if (!authored_href) {
continue;
}
const url = new URL(authored_href, window.location.href);
if (start_address === null) {
url.searchParams.delete("saddr");
} else {
url.searchParams.set("saddr", start_address);
}
link.href = url.toString();
}
}
document.addEventListener("ome:map-address-change", (event) => {
if (!(event instanceof CustomEvent)) {
return;
}
const { target_id, address } = event.detail;
start_addresses.set(target_id, address?.label ?? null);
});
document.addEventListener("click", (event) => {
if (!(event.target instanceof Element)) {
return;
}
const reset = event.target.closest("[data-directions-address-reset]");
if (!(reset instanceof HTMLElement)) {
return;
}
const target_id = reset.dataset.directionsTargetId;
if (target_id) {
start_addresses.set(target_id, null);
}
});
document.addEventListener("ome:facet:target-content-updated", (event) => {
if (
!(event instanceof CustomEvent) ||
!(event.target instanceof HTMLElement)
) {
return;
}
update_direction_links(event.target, event.detail.target_id);
});
function enhance_existing_targets() {
for (const target of document.querySelectorAll(".dealer-results")) {
if (target instanceof HTMLElement) {
update_direction_links(target, "dealers");
}
}
}
enhance_existing_targets();
Add the authored .dealer-results class to the dealer Facet Target. If the page also has a global Reset Facet, add your own data-directions-address-reset and data-directions-target-id="dealers" attributes to that authored reset control. Global Reset clears controls silently, so this optional click handler clears the integration's state before the next result update without depending on OME-owned attributes.
The helper runs once for the initial server-rendered DOM; the lifecycle listener handles every later replacement or append. Calling the function repeatedly is safe because URL.searchParams.set() replaces the existing value instead of duplicating it. Clearing Map Address directly or through the authored Reset integration stores null, and the next content update removes saddr.
Initial URL hydration does not emit a value change event. If another integration already knows an initial human-readable address, seed start_addresses before calling enhance_existing_targets().
This recipe runs before the Map runtime consumes the new Map POI selected content. A MutationObserver and response-payload mutation are not needed.
Example: Observe Response Counts
document.addEventListener("ome:facet:response", (event) => {
if (
!(event instanceof CustomEvent) ||
event.detail.target_id !== "products"
) {
return;
}
window.dataLayer?.push({
event: "facet_results_updated",
result_count: event.detail.count,
});
});
Example: Read Map Viewport State
document.addEventListener("ome:map:bbox-change", (event) => {
if (!(event instanceof CustomEvent)) {
return;
}
const { bounds, center, zoom } = event.detail;
console.log({ bounds, center, zoom });
});
Legacy Compatibility Fields
Existing integrations can continue to receive the historical fields below. New integrations should use the semantic snake_case fields and event.target instead.
| Event family | Legacy compatibility fields |
|---|---|
| Select | selectId, selectedValue, selectedValues, previousValue, previousValues, highlightedValue, rootElement, triggerElement, contentElement, itemElement |
| Search Select | searchSelectId, previousQuery, the Select-style value fields, rootElement, inputElement, triggerElement, contentElement, itemElement |
| Radio List | selectedValue, previousValue, rootElement, itemElement |
| Map Address | target, label, string lat, string lng, providerId |
| Map Radius | target, string value, string unit |
These fields are preserved for backwards compatibility, but their DOM references and presentation-oriented shapes should not be used as the basis of a new integration.