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.
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", { … })andwindow.SlaterSearch.refresh("site")—loaded: truetells you the API is live rather than the stub.
The hooks
| Hook | Runs | Signature |
|---|---|---|
transformItems | After 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 |
onItemRender | Per card, after all data-slater-bind-* bindings, just before the card is inserted. | (card, item, ctx) => void |
onRender | After 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:
transformItemsruns once per fetched page; the on-screen set accumulates transformed pages. - Back/forward cache: a restored page replays the already-transformed items;
onItemRender/onRenderfire again (re-apply your DOM annotations),transformItemsdoes not. - Isolation: a hook that throws is logged (a
[slater-search]warning) and skipped — the block renders as if the hook weren't there.transformItemsreturning anything but an array (including a Promise from anasyncfunction) 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
itemsandctx.configas 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 inonItemRender(or map to new objects intransformItems). - Filtering client-side can't change what the server counted:
data-slater-countkeeps 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>