These docs are a work in progress and may not be fully up to date. Some pages may contain internal notes for our team.
Skip to Content

External widget — stores and state

The widget uses Svelte’s built-in reactive store primitives. All stores are defined in src/lib/stores.js. There are no derived stores — every store is a writable. State flows in one direction: the CDN payload (or preview bridge payload) is applied via initializeFinderStores, which sets all stores in a single coordinated pass. Components subscribe reactively; they do not write to stores directly except through two exported helper functions.

Store roster

ExportInitial valuePurpose
payloadStorenullFull enriched payload object (finder + locations + parsed sub-objects)
designStore{}Design tokens extracted from payload.finder.settings (keys prefixed design.)
settingsStore{}Runtime settings extracted from payload.finder.settings (keys prefixed settings.)
localizationStorebuildLocalizationRuntime()Locale string + translation overrides; derived from design.localization
searchQueryStore''Current text in the search input
filterPanelOpenStorefalseWhether the filter/refinements panel is open
refinementsStoreSee belowAll filter, sort, and refinement state
cachedLocationsStore[]Full location array from the payload (never filtered)
locationsStore[]Visible (filtered + sorted + distance-annotated) locations
activeLocationStorenullCurrently selected/highlighted location
userLocationStore{ lat: null, lng: null, isGeolocated: false, error: null }Visitor’s resolved coordinates

All stores are declared at module scope with writable() (stores.js:5–40). There are no Svelte derived stores in this file — reactivity is triggered by explicit .set() and .update() calls inside initializeFinderStores and refreshVisibleLocations.

refinementsStore initial shape

stores.js:11–31:

export const refinementsStore = writable({ selectedTags: [], selectedCountries: [], selectedAdvancedRefinements: {}, radius: 500, radiusEnabled: true, sortBy: 'distance', tag: true, countries: true, sort: true, distance: true, radiusRange: true, popup: true, quickFiltersRow: true, advanced_refinements: [], filterOptions: [], sortOptions: [], selectedHoursOpen: false, selectedRatingMin: null, sortDirection: 'asc', });

The tag, countries, sort, distance, radiusRange, popup, and quickFiltersRow fields are visibility toggles that control which UI controls render. They are overwritten from payload.finder.settings entries with the refinements. prefix during initialization.

Initialization: initializeFinderStores

stores.js:229–348 — The single entry point for loading a payload into all stores. Called by App.svelte:190 via applyPayload. It accepts the raw payload and an options object { preserveUiState }.

Settings parsing

stores.js:235–251 — Every entry in payload.finder.settings is a flat { name, value } object. initializeFinderStores splits on . and routes entries into three nested objects by prefix:

  • design.*design object → designStore
  • settings.*settings object → settingsStore
  • refinements.*refinements object → merged into refinementsStore

Values are normalized by normalizeSettingValue (stores.js:107–112): the strings "true" and "false" become booleans; numeric strings become numbers.

Rules config parsing

stores.js:252–265 — After settings are parsed, parseRulesConfig processes design.refinements.rulesConfig (a JSON string) to build the filterOptions and sortOptions arrays. These arrays control which filter panels and sort options are visible and in what order. The default options are defined by defaultFilterOptions and defaultSortOptions (stores.js:114–134).

The default sort is determined by finding the first sortOptions entry that is both enabled and isDefault; its id and direction are written into refinements.sortBy and refinements.sortDirection.

Payload enrichment

stores.js:267–279 — The raw payload is re-assembled with the parsed sub-objects attached under payload.finder:

const enrichedPayload = { ...payload, finder: { ...payload.finder, design, // parsed design tokens parsedSettings: settings, parsedRefinements: refinements, }, }; payloadStore.set(enrichedPayload);

App.svelte subscribes to payloadStore (App.svelte:96–104) to extract customCSS.

preserveUiState branching

stores.js:281–347 — When preserveUiState is true (Live Preview hot-reload, see preview-bridge.md), initializeFinderStores keeps visitor UI state intact:

  • searchQueryStore and filterPanelOpenStore are not reset.
  • refinementsStore is updated with payload-sourced values but selected tags, countries, advanced refinements, hours filter, rating filter, radius, and sort are preserved from the current store value.
  • A validity check confirms the preserved sortBy is still enabled in the new sortOptions; if not, it falls back to the payload default.
  • pruneSelections (stores.js:211–227) removes any selected tags, countries, or advanced refinement names that no longer exist in the new locations array.

When preserveUiState is false (initial load or full remount), all selections are reset to defaults.

Location change detection

stores.js:290–347 — Locations are compared by a stable JSON signature (getLocationSignature, stores.js:193–209) that covers id, title, latitude, longitude, address, tags, and custom_fields. If the signature is unchanged from the last call (e.g., a design-only change in preview mode), cachedLocationsStore is not updated and activeLocationStore is preserved without scroll-to-top behavior.

// stores.js:291 const nextLocationSignature = getLocationSignature(locations); const locationsChanged = nextLocationSignature !== lastLocationSignature;

lastLocationSignature is module-level state (stores.js:42) — it persists across calls to initializeFinderStores within the same widget lifecycle.

refreshVisibleLocations

stores.js:44–91 — Private function called by initializeFinderStores and setUserLocation. It reads the current values of all stores via get(), calls applyFinderFilters from filtering.js, writes the result to locationsStore, then resolves the correct activeLocationStore value:

  1. If the previously active location still exists in the filtered list, it stays active.
  2. Otherwise, if preserveActiveLocation is true, the old activeLocationStore value is kept even if it’s not in the visible list (edge case: radius filter hides the active location).
  3. Otherwise, the first location in the filtered list becomes active.

setUserLocation (exported)

stores.js:350–359 — The only public write path for userLocationStore. Called by App.svelte after the geolocation or IP-fallback resolves. After updating userLocationStore, it triggers refreshVisibleLocations() so distance is recalculated and the list re-sorts immediately.

getCurrentLocations (exported)

stores.js:361–363 — Synchronous snapshot of locationsStore via get(). Used by analytics and the map component to read the current visible set without subscribing.

App.svelte subscriptions

App.svelte subscribes to three stores at module scope using Svelte’s $: syntax shorthand (.subscribe call):

  • payloadStore.subscribe (App.svelte:96) — extracts customCSS
  • localizationStore.subscribe (App.svelte:106) — keeps localization local variable in sync for template helpers
  • designStore.subscribe (App.svelte:110) — keeps design local variable in sync for layout reactive declarations

Components (Search, Map, List, MobileDetail) subscribe to individual stores via Svelte’s $store auto-subscription syntax in their own <script> blocks.

Filtering logic (filtering.js)

src/lib/filtering.jsapplyFinderFilters is called by refreshVisibleLocations. It:

  1. Annotates every location with a distance field (Haversine formula, filtering.js:3–26).
  2. Applies text search (simple substring or natural-language mode).
  3. Applies tag, country, radius, hours-open, and minimum-rating filters.
  4. Applies advanced refinement rules (show/hide actions with field conditions).
  5. Sorts via sortLocations (distance, name, country, state, city, postal, hours).

No filtering state lives in filtering.js itself — it is a pure function that takes all inputs as arguments and returns a new array.

Persistence

There is no localStorage or sessionStorage persistence for store values. The localStorage layer in cdn-and-pointer.md caches the raw CDN payload by hash, but that is read once at boot (in api.js) and never written by store code. All store state is ephemeral — it resets on page reload.