Skip to content
MapSVG 9/Documentation/Customization

JavaScript, middleware, and events

Use JavaScript to run custom behavior at a specific point in the App lifecycle. Choose middleware to transform data before it is consumed, an event handler to react to something that happened, or a template script to initialize rendered content.

This page preserves the middleware examples, event families, and FAQ from the v8 JavaScript guide, with the v9 Code picker workflow and current payloads. The JavaScript API reference provides additional method details. The AI assistant can help explain or draft code when its module and connection are available; review its output against the actual source and preview.

  1. Open the App panel and select the owner of the behavior.
  2. Open Code and expand the source picker.
  3. Choose the required source under JS.

App-wide initialization events, repository events, and middleware are available on App. Region and Marker expose their own interaction handlers. Template owners expose the script associated with each template. The picker displays readable source labels without the old @event / @middleware suffixes.

Enter a bare function, preserving the selected source’s parameter signature:

function(event) {
const { map, data } = event;
console.log(event.name, map.id, data);
}

Do not paste an assignment such as const handler = ..., an extra wrapping pair of parentheses, or a <script> element. JavaScript editing requires the Code editor module and the appropriate WordPress code-editing capability. See Code editor overview.

Middleware sits between steps in a process. MapSVG passes data to the function, takes its return value, and continues with that result. Use it to adapt input, format display data, validate a value, or log a request.

For example: MapSVG prepares a data request → your request middleware adjusts the data → MapSVG sends the request. Controller rendering follows the same idea: controller data → render middleware → template HTML.

The common signature is:

function(data, ctx) {
// Inspect or transform data here.
return data;
}

data is the value being processed. ctx contains the context for that particular middleware. Always return the original or replacement data, including branches where no change is needed. These middleware functions run synchronously; do not return a Promise expecting the pipeline to await it.

Choose App → Code → JS → Map init. Its stored middleware key is mapLoad. It receives map options before they are applied, with ctx.map identifying the instance.

For example, disable zoom controls only on a particular frontend URL:

function(data, ctx) {
const { map } = ctx;
if (!map.inBackend && window.location.href.includes("/a-post-about-cats")) {
data.controls = { ...data.controls, zoom: false };
}
return data;
}

You can use other runtime conditions, such as page context or a server-provided user role, in the same way. Browser-side conditions are presentation logic, not authorization checks.

Choose App → Code → JS → Controller render. Its key is render; context contains controller. It transforms the controller’s data before Handlebars renders it. It does not update the Collection record.

Use controller.is(...) to distinguish details, popover, tooltip, directory, or filters. details is accepted as an alias for detailsView.

This example formats a Details View amount from a number or numeric string such as 1000000 to 1,000,000:

function(data, ctx) {
const { controller } = ctx;
const amount = Number(data.totalAmount);
if (controller.is("details") && data.totalAmount != null && Number.isFinite(amount)) {
return {
...data,
totalAmount: amount.toLocaleString("en-US")
};
}
return data;
}

Converting a numeric string with Number(...) matters: calling toLocaleString directly on a string does not format it as a number. For the current Details interaction limitation, test the same transformation with controller.is("popover") and a Popover rather than depending on Show details view.

Choose App → Code → JS → API request. Its key is request. Context contains request, repository, and map. It can adapt outgoing repository request data to a compatible custom API.

Retaining the v8 example: for the users repository’s index action, append _bar to the foo parameter:

function(data, ctx) {
const { request, repository, map } = ctx;
if (repository.schema.objectNamePlural === "users" && request.action === "index") {
return { ...data, foo: String(data.foo ?? "") + "_bar" };
}
return data;
}

index is the list operation. Replace users with the actual schema’s plural name. This hook belongs to MapSVG’s repository pipeline; it is not an interceptor for every fetch or AJAX call on the WordPress page. V9 Page bindings still use named Queries and their supported input contract. A custom parameter only has an effect if the destination API understands it; it does not change a shared Query definition by itself.

The v9 picker also exposes API response, key response. Context contains request, response, repository, and map. Return transformed response data, not the entire context or response wrapper.

For a compatible custom repository whose index endpoint returns items, page, and totalPages, an adaptation can be:

function(data, ctx) {
const { request, repository } = ctx;
if (repository.schema.objectNamePlural !== "users" || request.action !== "index") {
return data;
}
const source = typeof data === "string" ? JSON.parse(data) : data;
return {
users: {
items: source.items,
hasMore: source.page < source.totalPages
}
};
}

This illustrates the repository response adapter contract, not the format for replacing the v9 named-Query HTTP protocol. Keep adapters specific to the source that needs them.

A configured event handler receives one event object:

function(event) {
const { map, data, name, domEvent } = event;
console.log(name, data);
}
  • map is the current MapSVG instance.
  • data is the event-specific payload; its fields vary by event.
  • name is the event name.
  • domEvent is the original pointer/touch event when available. Initialization and data events need not have one.

Use the instance supplied by the event instead of a global map variable. This keeps separate Apps and multiple embeds from targeting each other accidentally.

The runtime exposes more events than every component’s visible picker. If a lifecycle event described below is not offered as a source, subscribe to it from App → Code → JS → afterInit:

function(event) {
const { map } = event;
map.events.on("afterShow.popover", function(popoverEvent) {
const { controller } = popoverEvent.data;
console.log("Popover opened", controller);
});
}

Use one configured handler per purpose. Re-running an afterInit experiment that attaches listeners can add another subscription; reload the preview between experiments, or retain the exact callback and remove it with map.events.off(name, callback) before registering again. Do not call off(name) indiscriminately: it removes all listeners for that name, including ones you did not add.

Runs early, before the map completes its initialization. The MapSVG instance exists, but loaded Regions, markers, controllers, and their DOM may not be ready.

function(event) {
const { map } = event;
console.log("Starting map", map.id);
}

Use it for early setup that does not need those resources. Use Map init middleware to transform the options being applied.

Runs after the map’s initialization. Use it for setup requiring the initialized map, such as connecting a custom modal or adding runtime subscriptions.

function(event) {
const { map } = event;
console.log("Map initialized", map.id);
}

Initialization and subsequent data refreshes are different boundaries. Use afterLoadRegions or afterLoadObjects when your code specifically depends on a newly loaded dataset.

Runs after Region data has been loaded and applied to SVG Regions. Use it for work that needs those Regions or for updating content dependent on them.

function(event) {
const { map, data: { regions } } = event;
console.log("Regions loaded", regions);
console.log("Current map Regions", map.regions);
}

A Directory using the Regions binding participates in the data-refresh flow. This event is not a guarantee that every separate DOM widget or external service has finished rendering; use the relevant controller lifecycle for DOM-dependent work.

Runs after Objects are applied to the map’s current data context, including their marker/Region connections and the corresponding data-driven updates.

function(event) {
const { map, data: { objects } } = event;
console.log("Loaded Objects", objects);
console.log("Repository Objects", map.objectsRepository.getLoaded());
}

Use it to respond to refreshed markers, linked Objects, or Object-based content. The current source of loaded Objects is map.objectsRepository.getLoaded(); do not copy the old guide’s map.objects expression as if it were the loaded repository API.

Runs when the geographic map zoom changes:

function(event) {
const { map } = event;
console.log("Map zoom level", map.zoomLevel);
}

Use the level to adjust elements, report zoom, or decide when to request additional content. Editor canvas zoom is a different operation; scaling the device canvas does not test this event.

A repository loads and holds the Regions or Objects used by the map. Its source can be a MapSVG Collection, WordPress posts, or another supported API. A runtime repository is not a separate copy of your shared Collection definition.

Runs before a repository load proceeds. Inspect its context or log the operation:

function(event) {
const { map, data: { repository } } = event;
console.log("Loading", repository.schema.objectNamePlural);
}

Runs when a repository load completes:

function(event) {
const { map, data: { repository } } = event;
console.log("Loaded", repository.schema.objectNamePlural, repository.getLoaded());
}

These are repository boundaries. For geometry/marker work after MapSVG has applied the records, use the corresponding afterLoadRegions or afterLoadObjects event instead.

Region interaction handlers receive the Region instance at event.data.region. Select Region → Code → JS for the offered handlers.

Runs when a Region is clicked:

function(event) {
const { map, data: { region } } = event;
console.log("Clicked Region", region.id, region.getData());
}

Runs when the pointer enters a Region:

function(event) {
const { map, data: { region } } = event;
console.log("Entered Region", region.id);
}

Runs when the pointer leaves a Region:

function(event) {
const { map, data: { region } } = event;
console.log("Left Region", region.id);
}

Test these in Arrow mode. Hand intercepts canvas pointer input, and disabled Regions do not behave like ordinary interactive Regions.

Marker handlers receive a Marker instance at event.data.marker. Select Marker → Code → JS. A Marker is the visual representation of an Object with a Location; it is not itself the Collection record.

function(event) {
const { map, data: { marker } } = event;
console.log("Clicked Marker", marker.id, marker.object);
}
function(event) {
const { map, data: { marker } } = event;
console.log("Entered Marker", marker.id);
}
function(event) {
const { map, data: { marker } } = event;
console.log("Left Marker", marker.id);
}

Directory events depend on the Directory module and its active Page binding. Current interaction payloads can contain regions (an array), object, marker, and directoryItem. A Region-based row need not have an Object or Marker. Do not assume the old example’s singular region exists for every row.

When a named Directory event is not offered in the Code picker, register it from afterInit as described above.

Runs when a Directory row is clicked. To preserve the v8 example’s behavior—show only the clicked Object’s marker—use this event callback:

function(event) {
const { map, data: { object, marker, directoryItem } } = event;
const selectedMarker = marker || object?.location?.marker;
if (!selectedMarker) return;
map.markers.forEach(function(candidate) {
if (String(candidate.id) === String(selectedMarker.id)) {
candidate.show();
} else {
candidate.hide();
}
});
}

This changes marker visibility in the current runtime. It does not delete records or save a filter. Decide when to restore other markers if visitors should be able to return to the full result set.

The v8 guide listed this event without an example. It runs on entering a Directory row and uses the same row-context shape:

function(event) {
const { map, data: { regions, object, marker, directoryItem } } = event;
console.log("Entered Directory row", object || regions, directoryItem);
}

Runs on leaving the row:

function(event) {
const { map, data: { regions, object, marker, directoryItem } } = event;
console.log("Left Directory row", object || regions, directoryItem);
}

Details View, Popover, and Tooltip expose events around rendering, opening, and closing. Their payload identifies the current controller:

function(event) {
const { map, data: { controller } } = event;
console.log(event.name, controller);
}

The suffix identifies the controller family. These events describe lifecycle points, not every possible visibility change caused by unrelated CSS. Use a template’s afterRender script for setup that belongs to that rendered template.

The complete event names are retained:

  • beforeRedraw.detailsView: before content is rendered again.
  • afterRedraw.detailsView: after the redraw.
  • beforeShow.detailsView: before opening.
  • afterShow.detailsView: after opening.
  • beforeClose.detailsView: before closing.
  • afterClose.detailsView: after closing.

For example, register an event from App’s afterInit:

function(event) {
const { map } = event;
map.events.on("afterRedraw.detailsView", function(viewEvent) {
const { controller } = viewEvent.data;
console.log("Details content redrawn", controller);
});
}

These contracts do not resolve the current Show details view instance-selection problem. See Details View; avoid that action as a required step until fixed.

  • beforeRedraw.popover: before rendering new content.
  • afterRedraw.popover: after rendering.
  • beforeShow.popover: before opening.
  • afterShow.popover: after opening.
  • beforeClose.popover: before closing.
  • afterClose.popover: after closing.

Example in App’s afterInit:

function(event) {
const { map } = event;
map.events.on("afterClose.popover", function(popoverEvent) {
const { controller } = popoverEvent.data;
console.log("Popover closed", controller);
});
}
  • beforeRedraw.tooltip: before rendering new content.
  • afterRedraw.tooltip: after rendering.
  • beforeShow.tooltip: before showing.
  • afterShow.tooltip: after showing.
  • beforeClose.tooltip: before closing.
  • afterClose.tooltip: after closing.

The close names above are retained from the v8 reference, but normal hover-tooltip removal currently destroys the Tooltip instead of passing through the closable controller’s close() path. Do not depend on beforeClose.tooltip / afterClose.tooltip firing on every pointer exit. Use the Region/Marker mouseout event for that user interaction, or own cleanup alongside your rendered instance.

Example in App’s afterInit:

function(event) {
const { map } = event;
map.events.on("afterShow.tooltip", function(tooltipEvent) {
const { controller } = tooltipEvent.data;
console.log("Tooltip shown", controller);
});
}

A template script uses a different signature from event handlers:

function({ el, map, controller }) {
const button = el.querySelector(".office-contact");
if (button && !button.dataset.mapsvgBound) {
button.dataset.mapsvgBound = "true";
button.addEventListener("click", function() {
console.log("Contact clicked in map", map.id);
});
}
}

el is the current rendered template element, map its App runtime, and controller the containing view. Work relative to el, especially inside Shadow DOM. Content can be rendered repeatedly when visitors choose another record; initialize each rendered instance rather than adding another document-wide listener on every redraw.

Directory’s directory, directoryItem, and categoryItem sources each retain their own HTML/CSS/JS. Template code is not a substitute for an initialization handler, and an initialization handler does not run after every template redraw.

How do I modify map settings before they are applied?

Section titled “How do I modify map settings before they are applied?”

Use Map init middleware, returning the original or adjusted options. Keep the map.inBackend guard for frontend-only changes.

How do I change data passed to Details View, Popover, or Tooltip?

Section titled “How do I change data passed to Details View, Popover, or Tooltip?”

Use Controller render middleware. It changes rendering input, not stored Collection values.

Use afterInit to initialize your modal integration, then react to the appropriate Region/Marker event. This is the same integration point described in v8. For an existing modal plugin, load its dependency and call that plugin’s documented API; MapSVG does not provide a universal jQuery .modal() method.

For example, the following afterInit handler creates a native dialog with jQuery and opens it on a Region click. It demonstrates the custom-modal workflow without depending on the built-in Details action:

function(event) {
const { map } = event;
const doc = map.containers.root.ownerDocument;
const dialog = doc.createElement("dialog");
const $dialog = jQuery(dialog);
$dialog.append("<h3></h3><p></p><button type='button'>Close</button>");
doc.body.appendChild(dialog);
$dialog.find("button").on("click", function() { dialog.close(); });
map.events.on("click.region", function(regionEvent) {
const { region } = regionEvent.data;
const record = region.getData();
$dialog.find("h3").text(record.title || region.id);
$dialog.find("p").text(record.description || "");
if (!dialog.open) dialog.showModal();
});
}

Own the dialog’s lifecycle if the surrounding application mounts/unmounts maps dynamically. Reload the preview between editing experiments to discard that temporary DOM. If replacing a built-in click action, review the Region’s configured Interactions so both actions do not open competing views.

Use a separate draft. For initialization/middleware edits, reload the preview to exercise the full lifecycle. For interaction edits, select Arrow, test several records, and inspect the browser console. Repeat on the real frontend page with the intended modules and third-party dependencies.

Draft text can contain an invalid function even while an earlier valid event callback remains active in the editor. A successful autosave is not proof that the new function executed. Correct errors, reload, then Publish and verify the live page separately.