JS hooks (advanced)

Per-project custom functions. Your own JavaScript - a Webflow code block or a hosted file - registers callbacks, and the embed invokes them at fixed lifecycle points: reshape a page of results before it renders, annotate each card, or react after a render.

Shared setup - the script tag, keys, and data binding - is in the getting started guide.

Everything else in the embed is no-code; hooks are the escape hatch. Reach for filters / base filters first — a server-side filter keeps counts and pagination truthful, a hook does not (see Limits below).

Registering

One canonical snippet — it works regardless of load order, before or after the embed boots (the queue is drained at boot; after boot q.push executes immediately):

<script>
  window.SlaterSearch = window.SlaterSearch || { q: [] };
  window.SlaterSearch.q.push(["registerHooks", "site", {
    transformItems: (items, ctx) => items.filter((i) => i.collection !== "locations"),
    onItemRender: (card, item, ctx) => card.classList.add("is-" + item.collection),
    onRender: (ctx) => console.log(ctx.rendered + " of " + ctx.total),
  }]);
</script>

The second element is the block key (the data-slater-list / data-slater-search-list value), or "*" for every block on the page.

Equivalent forms:

  • Config block (for scripts that run before the embed tag): window.SlaterSearchConfig.hooks = { site: { … }, "*": { … } }.
  • Live API (after boot): window.SlaterSearch.registerHooks("site", { … }) and window.SlaterSearch.refresh("site") — loaded: true tells you the API is live rather than the stub.

The hooks

HookRunsSignature
transformItemsAfter a page of items arrives (and, in search mode, is normalized), before it renders. Return the array to render — filter, reorder, or map.(items, ctx) => items
onItemRenderPer card, after all data-slater-bind-* bindings, just before the card is inserted.(card, item, ctx) => void
onRenderAfter a page of cards is in the document and the chrome (count, empty, load-more) has settled.(ctx) => void

ctx is { key, mode, query, config } — mode is "replace" or "append" (load-more) — and onRender additionally gets { total, rendered } (server total and cards on screen across all loaded pages).

Semantics

  • Merge: effective hooks for a block are "*"'s, overridden per hook name by the block-specific ones. Registering the same key twice merges per hook name — later wins — so separate scripts can each contribute different hooks.
  • Late registration (the normal case for hosted files): registering after boot re-runs the matching block(s) — usually served from the embed's response cache, so no extra network — and resets pagination to the first page. Search blocks with an empty query stay idle.
  • Load more: transformItems runs once per fetched page; the on-screen set accumulates transformed pages.
  • Back/forward cache: a restored page replays the already-transformed items; onItemRender/onRender fire again (re-apply your DOM annotations), transformItems does not.
  • Isolation: a hook that throws is logged (a [slater-search] warning) and skipped — the block renders as if the hook weren't there. transformItems returning anything but an array (including a Promise from an async function) is ignored the same way. Hooks are synchronous — nothing is awaited.
  • Hooks never run on Webflow's server-rendered fallback cards (failure degradation), on the idle/clear path, or on pages with no blocks — though the API object exists everywhere, so a site-wide snippet is always safe.

Limits

  • Treat items and ctx.config as read-only — item objects are shared with the embed's response cache, so an in-place mutation leaks into later cache hits. To change what a card shows, annotate the DOM in onItemRender (or map to new objects in transformItems).
  • Filtering client-side can't change what the server counted: data-slater-count keeps showing the server total, and load-more paging follows server offsets. If the rule can be expressed as a field filter, use a base filter instead.

Example: per-project result gating

Hide locations results unless the item's link-to-post switch is on — for just this project, from its own code block:

<script>
  window.SlaterSearch = window.SlaterSearch || { q: [] };
  window.SlaterSearch.q.push(["registerHooks", "site", {
    transformItems: (items) =>
      items.filter((i) => i.collection !== "locations" || i["link-to-post"] === true),
  }]);
</script>