<goma-region-filter>
Horizontal pill row of regions (countries) with a hardcoded "All" pill on the left, single-selection state, and an optional flag glyph per pill. It sits between a sport picker and a matches feed: it fetches its region list scoped to a sport, and selecting a pill hands a locationId filter downstream to widgets like <goma-events-horizontal>.
Live demo
Open in the playground →: toggle Region filter in the widget rail, then switch between mock, rpc, and subscribe sources in the inspector. The mock fixture (playground-only, not a real dataSource value) surfaces a small set of regions with drifting live counts so you can verify the pill UX without a real WAMP feed.
Element tag
html
<goma-region-filter></goma-region-filter>Installation
@gomagaming/region-filter is a scoped private package. See Installation for the one-time .npmrc setup required to consume it.
bash
npm install @gomagaming/region-filterYou also need the shared peer dependencies installed at your app level:
bash
npm install vue pinia vue-router vue-i18n @vueuse/core autobahn-browser swiperConfiguration
Assign a config object to the element's config property:
js
const el = document.getElementById('rf')
el.config = {
socketUrl: 'wss://sportsapi.example.com/v2',
socketRealm: 'www.example.com',
locale: 'en',
}| Key | Type | Required | Description |
|---|---|---|---|
socketUrl | string | Yes | WAMP socket URL. The widget hits /sports#locations over this transport. |
socketRealm | string | Yes | WAMP realm. |
sportsNamespace | string | No | Root WAMP namespace the getLocations RPC is built against ({sportsNamespace}#locations). Defaults to '/sports'. |
network | object | No | Network policy; network.rpcTimeoutMs bounds each try of the getLocations call (up to 3 tries in 'rpc' mode, with a 2 s and a 5 s wait between them, so the worst case is 3 x rpcTimeoutMs + 7 s, about 52 s by default). A failed or timed-out try is logged. When all 3 failed, the skeleton goes away and regions keeps the list of the same sport, or is empty. |
locale | string | No | i18n locale; also sent as the RPC's lang kwarg. Defaults to 'en'. |
theme | object | No | CSS variable overrides: see Theming. |
messages | object | No | i18n overrides per locale. |
debug | boolean | No | Verbose console logging (also gates the followSportPicker rewrite log, see Integrates with). |
apiBaseUrl, bettingApiBaseUrl, and ucsOperatorId are accepted on the shared config shape but this widget's RPC path never reads them: it only needs the WAMP socket to reach getLocations.
Data sources
The dataSource property selects how the region list is fetched. The hardcoded "All" pill (see below) is always present regardless of mode.
'rpc': one-shot WAMP call (default)
The widget calls getLocations({ sportId }) (a thin alias over the underlying getAllLocations, which issues apiSession.$wcall('/sports#locations', { sportId, lang })) once the WAMP connection is up, mirrors the result into state.regions, then renders. Re-runs whenever sportId changes (including via followSportPicker).
js
el.dataSource = 'rpc'
el.sportId = 1
el.config = { /* socket creds */ }'subscribe': live WAMP subscription (placeholder)
Note: the backend topic for live region updates is not yet available. The widget accepts
dataSource: 'subscribe'so the public surface is final, but internally it falls back to the one-shot RPC and logs a singleconsole.warnthe first time (subsequent calls stay silent). It makes one try only: the 3 tries andgoma:feed-load-failedapply in'rpc'mode. Once the topic ships, this falls back automatically: no widget-facing change.
js
el.dataSource = 'subscribe'
el.sportId = 1
el.config = { /* socket creds */ }GomaRegion shape
ts
interface GomaRegion {
id: number
name: string
shortName?: string // displayed in the pill: falls back to `name`
code?: string // ISO 3166-1 country code
numberOfEvents?: number // the badge for a live + prelive list
numberOfLiveEvents?: number // the badge for a live list
numberOfMarkets?: number
numberOfBettingOffers?: number
numberOfLiveMarkets?: number
numberOfLiveBettingOffers?: number
numberOfUpcomingMatches?: number // the badge for a prelive list
contexts?: { popular?: 1 } // the locations RPC always tags entries 'popular'
}Produced by the shared mapLocation() mapper. The pill UI only reads id, name, shortName, code, and the counter picked by contextLiveStatus and counterFields.
Sort order
Regions with numberOfLiveEvents > 0 float to the top (sorted descending by live count); the rest preserve their original feed order at the tail. The "All" pill is always prepended ahead of the regions.
The pill counter
Each region pill can carry a small count badge. It should count the same events the page lists beside the pills: a live count next to a prelive-only list reads as a wrong number. Three settings control it:
| Setting | Default | What it does |
|---|---|---|
showCounter | true | false hides the badge. |
contextLiveStatus | 'BOTH' | Which events the page lists: 'BOTH', 'LIVE' or 'NOT_LIVE'. |
counterFields | see below | Which locations-feed field the badge reads for each contextLiveStatus. |
The default counterFields:
json
{
"BOTH": "numberOfEvents",
"NOT_LIVE": "numberOfUpcomingMatches",
"LIVE": "numberOfLiveEvents"
}A partial map overrides only the statuses it names. An unrecognised contextLiveStatus falls back to 'BOTH', and a count of 0 renders no badge.
Two limits to know about:
- The counters are per sport and per region, nothing narrower. They ignore any time window, competition or market filter the page applies to its list. With a time filter narrowed to "next 3 hours", the prelive counter still counts every prelive event of the region. If the page's list is filtered that way, set
showCountertofalse. - The counters are a snapshot. The list is fetched once per sport (see Data sources), so a count goes stale while the page stays open.
The hardcoded "All" pill
Always rendered as the leftmost pill, never sourced from the API. Its label is locale-driven (region_filter_all_label). Clicking it, or a direct state.sportId change auto-resetting the selection (see Integrates with), emits:
js
{
regionId: 'all',
regionName: 'All', // or the active locale's translation
regionShortName: 'All',
regionCode: null,
previousRegionId: <whatever was selected>,
}Hosts treat regionId === 'all' as the canonical "no filter" sentinel. This aligns with the matches-aggregator topic format where locationId = 'all' is the no-filter value: see Integrates with.
Props / attributes
| Property | Attribute | Default | Description |
|---|---|---|---|
dataSource | data-source | 'rpc' | 'rpc' / 'subscribe'. See Data sources. |
sportId | sport-id | 1 | Sport id passed to the locations RPC. Locations are always sport-scoped upstream. |
regions | - | [] | Array of GomaRegion objects. Normally populated by the WAMP composable; can be set directly by the host. |
selectedRegionId | selected-region-id | 'all' | Id of the currently-selected pill. 'all' selects the hardcoded sentinel. Two-way: a click also updates this value. |
showLabels | show-labels | true | Render the short label on each pill. |
showFlags | show-flags | true | Render the flag glyph on each region pill. The "All" pill never renders a flag. |
spaceBetween | space-between | 8 | Pixel gap between pills. |
skeletonCount | skeleton-count | 8 | Number of placeholder pills shown until the first regions assignment. |
flagBaseUrl | flag-base-url | https://static.glastcoper.com/omfe-widgets/s/assets/1.10.2/om1/icons/flag/ | Base URL the flag glyph is built off: final URL is ${flagBaseUrl}{regionId}.png. Override to point at your own host. |
showCounter | show-counter | true | Show the count badge on each pill. See The pill counter. |
contextLiveStatus | context-live-status | 'BOTH' | Which events the page lists beside the pills: 'BOTH', 'LIVE' or 'NOT_LIVE'. Picks the badge's field through counterFields. |
counterFields | counter-fields (JSON) | { BOTH: 'numberOfEvents', NOT_LIVE: 'numberOfUpcomingMatches', LIVE: 'numberOfLiveEvents' } | Locations-feed field the badge reads for each contextLiveStatus. A partial map overrides only the statuses it names. |
followSportPicker | follow-sport-picker | true | Listen at the window for goma:sport-select and re-fetch the region list for the picked sport. See Integrates with. |
sportPickerSelector | sport-picker-selector | null | CSS selector that scopes followSportPicker to a specific picker on the page (multi-instance disambiguation). |
resetRegionOnSportChange | reset-region-on-sport-change | true | Drop the selection back to 'all' whenever sportId changes. Set false when the host sets sport and region together: see Selecting a sport and a region together. |
Each region pill resolves its flag from ${flagBaseUrl}{regionId}.png; if the image fails to load (404, network error) the pill falls back to a globe glyph automatically.
Events
| Status | Event | Detail | Emitted when |
|---|---|---|---|
| Canonical | ready | {} | Widget mounted, initial render done. |
| Canonical | goma:connection | { state, attempt?, nextRetryMs?, lastError?, attempts? } | Connection state transition. |
| Canonical | goma:error | { message, code, component? } | Two paths. Connection layer: the WAMP adapter exhausts its 5 retry attempts (code: 'API_INIT_ERROR'), alongside goma:connection { state: 'failed' }. Widget root scope: an error raised in the widget's own outermost component (its setup, its render, its lifecycle hooks or its watchers), where code is the error's name, e.g. 'TypeError' ('RUNTIME_ERROR' when it has none). Errors raised inside the widget's child components do not reach this event: the in-shadow error boundary catches those and shows the retry banner instead, emitting only goma:retry. |
| Canonical | goma:region-select | { regionId, regionName, regionShortName, regionCode, previousRegionId } | Any change of selectedRegionId: a user tapping (or activating via Enter/Space) a region pill or the "All" pill, a sport change that forces the selection back to 'all' (see Integrates with), and a host write to the selectedRegionId property or attribute. Setting the value the widget already holds emits nothing. Never emitted on mount: the initial value is applied before the widget renders. |
| Canonical | goma:feed-load-failed | { source: 'locations', sportId } | The regions call failed on all 3 tries in 'rpc' mode (each bounded by network.rpcTimeoutMs, 15 s by default, with a 2 s and a 5 s wait between them). The skeleton goes away and the "All" pill stays. Informational: use it to show your own message or to reload. |
Because every path emits, a host-driven selection reaches <goma-events-horizontal followRegionPicker> exactly as a tap does; there is no need to rewrite the feed's topic by hand.
regionName, regionShortName and regionCode are resolved from the loaded region list. A selection applied before that list arrives (a deep link on first paint) still emits immediately with the correct regionId and null for the three name fields, since regionId is what downstream listeners read.
Integrates with
Listens for goma:sport-select (composed, bubbles past Shadow DOM), gated by followSportPicker (default true). An optional sportPickerSelector (default null, meaning "respond to the first picker on the page") scopes it to one emitter when several pickers coexist. Commonly pairs with <goma-sports-navigation-horizontal> or <goma-sports-navigation-dialog> upstream.
Every sportId change, whether driven by followSportPicker or set directly, refetches the region list. If the current selection isn't already 'all', it's reset to 'all' and goma:region-select fires with that reset, since locations are sport-scoped server-side and a previously-selected region almost certainly doesn't exist in the new sport's list. Set resetRegionOnSportChange to false to own that decision yourself.
Downstream, this widget's own goma:region-select is the event <goma-events-horizontal> listens for via its followRegionPicker prop, rewriting the locationId segment of its matches-aggregator topic in place. The 'all' sentinel emitted by the hardcoded All pill maps directly onto that topic's literal all no-filter segment. See Cross-widget wiring for the full rewrite behavior and emitter/listener matrix.
This widget reads and writes no shared-storage envelope (no betslip, favorites, or session state): see Shared state for the widgets that do.
Selecting a sport and a region together
A navigation link that picks both at once (a "Football › Brazil" entry in a side menu, or a deep link restored on refresh) has to set sportId and selectedRegionId together. Turn the sport-change reset off for that:
js
const el = document.getElementById('rf')
el.resetRegionOnSportChange = false
// A menu entry that carries both.
function openSportAndRegion(sportId, regionId) {
el.sportId = sportId
el.selectedRegionId = regionId // survives; emits goma:region-select
}
// A menu entry that carries only a sport: you clear the region.
function openSport(sportId) {
el.sportId = sportId
el.selectedRegionId = 'all'
}With the default resetRegionOnSportChange: true there is no write order that works. The reset reads selectedRegionId when its watcher flushes, by which point your value is already in place, so it is treated as a leftover from the previous sport and replaced with 'all', whether you set the region before or after the sport. Deferring the region write by a tick does leave the pill correct, but the reset has already emitted 'all' in the meantime, so a followRegionPicker feed ends up unfiltered while the pill shows a region: the two disagree. Turning the reset off is the only way to express "this region is valid for this sport".
The trade is that you now own the reset for sport-only navigation, as in openSport() above: otherwise a region from the previous sport stays selected after the switch.
Empty / loading / error states
- Skeleton (loading): pulsing placeholder pills render alongside the "All" pill from mount until the first
regionsassignment. Count is configurable viaskeletonCount. An explicit empty array counts as "arrived": it doesn't leave the skeleton running forever. - Empty: once an empty array arrives, only the "All" pill is shown.
- Error: render errors caught by the widget's error boundary surface in a themed banner (
role="alert") for 8 seconds with a Retry button that clears the error and emitsgoma:retry.
Theming
Override any CSS variable by passing a theme object. All tokens are namespaced with --goma-* and applied as inline styles on the host element.
js
el.theme = {
backgroundPrimary: '#03061b',
backgroundCards: '#1f2147', // inactive pill background
textPrimary: '#ffffff', // inactive pill label
highlightPrimary: '#ff6600', // selected pill background
highlightPrimaryContrast: '#ffffff', // selected pill label
separatorLine: '#3d425a', // flag-circle ring
liveCountBackground: '#ed4f63',
liveCountText: '#ffffff',
}Tokens used by this widget
| Token | Element |
|---|---|
--goma-backgroundPrimary | Rail background |
--goma-backgroundCards | Inactive pill background |
--goma-textPrimary | Inactive pill label |
--goma-textSecondary | Flag fallback glyph |
--goma-separatorLine | Flag-circle ring |
--goma-highlightPrimary | Selected pill background |
--goma-highlightPrimaryContrast | Selected pill label |
--goma-liveCountBackground | Count badge background |
--goma-liveCountText | Count badge text |
--goma-alertError | Render-error banner background |
See the Theming guide for the complete token reference.
i18n / message overrides
| Key | en default | Used for |
|---|---|---|
region_filter_aria | Region filter | aria-label on the pill row's tablist. |
region_filter_all_label | All | The hardcoded "All" pill's name/short-name, including the label used when a sport change auto-resets the selection. |
region_filter_live_events_count | No live events | {count} live event | {count} live events | aria-label on each pill's badge while it shows the live count (vue-i18n plural form, keyed by count). |
region_filter_upcoming_events_count | No upcoming events | {count} upcoming event | {count} upcoming events | Same, while it shows the prelive count. |
region_filter_events_count | No events | {count} event | {count} events | Same, while it shows the total count. |
retry | Retry | Label on the error banner's Retry button (shared key, not region-filter-specific). |
Override via el.config = { messages: { en: { region_filter_all_label: 'Everywhere' } } }.
Embedding examples
Vanilla HTML
html
<goma-region-filter id="rf"></goma-region-filter>
<script type="module">
import '@gomagaming/region-filter'
const el = document.getElementById('rf')
el.dataSource = 'rpc'
el.sportId = 1
el.config = {
socketUrl: 'wss://example.com/v2',
socketRealm: 'www.example.com',
locale: 'en',
}
el.addEventListener('goma:region-select', (e) => {
console.log('Region:', e.detail.regionShortName, e.detail.regionId)
})
</script>React
jsx
import '@gomagaming/region-filter'
import { useEffect, useRef } from 'react'
function RegionFilter({ config, sportId = 1 }) {
const ref = useRef(null)
useEffect(() => {
const el = ref.current
if (!el) return
el.dataSource = 'rpc'
el.sportId = sportId
el.config = config
const onSelect = (e) => console.log(e.detail)
el.addEventListener('goma:region-select', onSelect)
return () => el.removeEventListener('goma:region-select', onSelect)
}, [config, sportId])
return <goma-region-filter ref={ref} />
}Vue 3
vue
<script setup>
import '@gomagaming/region-filter'
import { ref, onMounted, watch } from 'vue'
const el = ref(null)
const props = defineProps(['config', 'sportId'])
onMounted(() => {
el.value.dataSource = 'rpc'
el.value.sportId = props.sportId ?? 1
el.value.config = props.config
})
watch(() => props.sportId, (id) => { if (el.value) el.value.sportId = id ?? 1 })
function onRegionSelect(e) {
console.log('Region:', e.detail.regionId)
}
</script>
<template>
<goma-region-filter ref="el" @goma:region-select="onRegionSelect" />
</template>Pairing with <goma-sports-navigation-horizontal>
html
<goma-sports-navigation-horizontal id="picker"></goma-sports-navigation-horizontal>
<goma-region-filter id="regions" follow-sport-picker></goma-region-filter>
<script type="module">
import '@gomagaming/sports-navigation-horizontal'
import '@gomagaming/region-filter'
const picker = document.getElementById('picker')
const regions = document.getElementById('regions')
const sharedConfig = { /* socket creds */ }
picker.config = sharedConfig
picker.dataSource = 'subscribe'
regions.config = sharedConfig
regions.followSportPicker = true // picks up sport-select events from the picker
</script>Accessibility
| Element | Role | Attributes |
|---|---|---|
| Pill row container | tablist | aria-label="Region filter" (i18n key region_filter_aria) |
| Region pill | tab | tabindex="0", aria-selected, aria-label="{name}" |
| Count badge | - | aria-label naming what is counted, "N event(s)", "N live event(s)" or "N upcoming event(s)", following the field the badge reads |
| Error banner | - | role="alert" for screen-reader announcement |
Keyboard: Tab to focus pills, Enter / Space to select. :focus-visible ring uses the --goma-highlightPrimary token.
What's deferred
- Real subscribe path. Once the backend exposes a live-update topic for locations,
subscribeToLocationswill be wired through and the one-time fallback warning will disappear. The public surface (thedataSource: 'subscribe'value) stays the same.
Integration Caveats
SSR/webview: importing this package outside a browser previously threw immediately; fixed with a guard on the shared element factory.
SSR/webview: the sport-picker
windowlistener attach/detach now has atypeof window !== 'undefined'guard, matching the sibling dialog widget's pattern.Initialization: this widget shares
@gomagaming/core's i18n singleton with every other widget on the page; mounting two widgets with differentlocalevalues on one page is not currently isolated per instance (a known constraint, not fixed in this pass).Configuring before the definition loads. Assigning
config(or any property) while the element is still an unknown element is safe: the value is replayed through the widget when it upgrades. That matters when loading from the CDN, whose loader imports widget modules asynchronously, so your script usually runs first. On release sets at 0.34.0 and earlier the value was silently discarded and the widget mounted with no configuration and a clean console: there, awaitwindow.__gomaCdn.readyfirst, or configure through attributes. See Setting a property before the widget's definition has loaded.
See Integration Caveats for the cross-widget picture.
Changelog
Current release: 0.37.11. Every GomaPulse package shares one version number and they are published together, so a release that changes nothing here still bumps this widget. Only the releases that changed it are listed below.
Every release that changed this widget, newest first. The latest is open; the rest expand.
0.37.9
The event lists and the competition lists now wait for a slow or failing provider before they show an empty state. An event list keeps its loading state for about 55 s, long enough for the connection layer to ask for the data again, and the watchdog reports a failure only for a topic that was subscribed. The competition, top competition and region calls make up to 3 tries in
'rpc'mode (top competitions also in its'subscribe'fallback), with a wait of 2 s before the second and 5 s before the third.network.rpcTimeoutMsbounds each try. A provider error now counts as a failed try:getCompetitions,getTopCompetitionsandgetAllLocationsreject instead of resolving with nothing. A late answer of an earlier try that fills the list ends the tries. When every try has failed, the widget shows its empty state (the region filter drops its skeleton) and emits the new informational eventgoma:feed-load-failed({ source, sportId?, topic? }), so a host can show its own message or reload. A sport change during the wait cancels the old tries, and the region call now ignores an answer for a sport the player has already left. The connection layer only gains a comment that links its retry timings to the event-list wait.The event list waits 55 s only after a request for data was sent, and 15 s when nothing could be requested. Load more now waits the same 55 s before its spinner clears, and the match page (game details) waits for the retries before it shows its not-found or empty state.
The suggested bet card (
dataSource: 'recsys') waits up to 55 s for the first answer once it has subscribed to the recommended events, instead of showing its empty state after 15 s. After an answer that still leaves no priced combo, or when no subscribe could be sent, it gives up after 15 s.
0.37.3
<goma-region-filter>: the badge on each region pill can count the same events the page lists beside the pills.The badge was hard-wired to the region's live event count (
numberOfLiveEvents), so a page listing prelive events next to the pills showed a number that never matched its list ("Ghana 4" over a single prelive fixture). Three new props control it:showCounter(attributeshow-counter, defaulttrue):falsehides the badge.contextLiveStatus(attributecontext-live-status, default'BOTH'): which events the page lists,'BOTH','LIVE'or'NOT_LIVE'.counterFields(attributecounter-fields, JSON): the locations-feed field the badge reads for each status. Default{ BOTH: 'numberOfEvents', NOT_LIVE: 'numberOfUpcomingMatches', LIVE: 'numberOfLiveEvents' }; a partial map overrides only the statuses it names.
Behaviour change: with the default
'BOTH'the badge now showsnumberOfEvents(every event the feed counts for the region) instead of the live count. A page that wants the old badge setscontextLiveStatus: 'LIVE'.The badge's
aria-labelfollows the field it reads, so a custom map is announced as what it counts. Two new locale keys in@gomagaming/core,region_filter_events_countandregion_filter_upcoming_events_count(en, fr, pt), cover it.
0.35.0
Every picker now announces a host-driven selection, not just a tap
Setting a picker's selection from the page (
el.selectedSportId = '8'for a menu entry, a deep link, state restored after a refresh) used to change the highlighted item silently, because the event was emitted from inside the click handler. Anything wired up with afollow*prop kept its previous filter, so the highlighted pill and the data underneath it disagreed. The only workaround was to forge the widget's own event onwindow, which breaks as soon as a*Selectorprop is set.All seven pickers now emit from a watcher on the selection itself, so a tap, an internal reset and a host write are indistinguishable:
<goma-region-filter>,<goma-top-competitions>,<goma-top-competitions-pills>,<goma-competitions-list>,<goma-competition-filter>,<goma-sports-navigation-horizontal>and<goma-sports-navigation-dialog>.@gomagaming/coregains the shareduseSelectionEmitcomposable they all use, so a picker added later inherits the behaviour rather than reimplementing it.Three rules hold across all of them. Nothing is emitted on mount, so a value seeded from markup (
<goma-region-filter selected-region-id="19">) opens on that scope without firing the chain during boot. Re-assigning the value already held emits nothing. And ids are the contract while names are best-effort: a selection applied before the widget's list has loaded still emits immediately with the right id andnulldescriptors, so deep links never have to wait for data.Two deliberate exceptions:
<goma-competitions-list>stays silent when the selection is CLEARED, since closing its drill-in detail has its own event and an empty selection would scope a following feed to nothing; and<goma-sports-navigation-dialog>inshowApplyButtonmode treats the selection as pending, emitting when Apply is pressed.Two behaviour changes come with the single emit point. Re-tapping the item already active no longer emits (it is not a change): in the dialog that still closes it, it just announces nothing. And
<goma-top-competitions>'sisFeaturedis now derived fromhighlightedImagesand the active variant rather than passed in from the tap, so a host write reports it as truthfully as a click.<goma-top-competitions-pills>also gainsresetCompetitionOnSportChange(defaulttrue), matchingresetRegionOnSportChangeon<goma-region-filter>: it clears the selection on asportIdchange, and with it on no write order lets one navigation action set a sport and a competition together.region-filter: let a host set sport and region together, and emit when it does
Adds
resetRegionOnSportChange(attributereset-region-on-sport-change, defaulttrue, existing behaviour is unchanged). Set it tofalsewhen a single navigation action picks both a sport and a region, such as a "Football › Brazil" side-menu link or a deep link restored on refresh.Previously no write order worked. The sport-change reset read
selectedRegionIdwhen its watcher flushed, by which time the host's new value was already in place, so it was mistaken for a leftover from the previous sport and replaced with theallsentinel, whether the region was set before or after the sport.goma:region-selectis now emitted from a single watcher onselectedRegionId, so a host write to the property or attribute emits it too, alongside pill taps and the sport-change reset. A host-driven selection therefore reaches<goma-events-horizontal followRegionPicker>the same way a tap does; before, it changed the pill silently and left the feed unfiltered, so the visible selection and the data it drove disagreed.Two behaviour changes come with that single emit point: setting
selectedRegionIdto the value it already holds emits nothing (re-tapping the active pill used to re-emit), and the name fields are resolved from the loaded region list. A selection applied before that list arrives still emits at once with the correctregionIdandnullnames. The widget remains silent on mount.
0.34.0
Stop a region list fetched for the wrong sport from overwriting the right one, and parse numeric strings assigned to numeric widget properties instead of silently replacing them with the widget's default
A
<goma-region-filter>configured for any sport other than Football issued two location requests instead of one: its own, correctly scoped to the configured sport, and a second one (scoped to Football) that nothing had asked for. Both wrote the same place, so the pills the widget rendered were whichever response happened to come back last. The same race reached<goma-competitions-list>, which groups its competitions by the same region data.The unrequested request came from the per-widget boot routine, which fetched locations with no sport at all and so fell back to sport
1. Locations are scoped to a sport on the server, and boot has no sport to scope them by, so it no longer fetches them: the only readers of that data are the widgets above, and each of them already fetches it for the sport it is showing.That removes the collision at its source. As a second line of defence, a locations payload now records which sport it was fetched for, and a widget ignores one belonging to a sport it is not showing. This matters whenever two scoped fetches can overlap (switching sport while a request is still in flight, for instance) and it is deliberately narrow: a payload with no recorded sport is still rendered, so the guard can only ever suppress data it can positively identify as belonging to somewhere else.
Separately, every numeric property on every widget (
sportId,skeletonCount,pageSize,spaceBetween,autoplayIntervaland the rest) used to accept a JavaScript number and nothing else. A numeric string was not parsed; it was discarded and quietly replaced by the property's default value.el.sportId = '8'therefore left the widget on sport1and fetched the wrong data, with nothing logged to say so. Three ordinary integration paths hand over a string: a router parameter, a value read out of a CMS or JSON configuration, and a framework re-render that stringifies its props. Numeric strings are now parsed. Values that are not a finite number and not a numeric string (null,undefined,'', booleans, objects,NaN,Infinity) still fall back to the default, as before. HTML attributes (sport-id="8") and theconfigobject were never affected by this and are unchanged.
0.32.0
Widgets no longer dispatch a bare
errorevent alongsidegoma:error.Every factory-built widget's app-level Vue error handler dispatched two events for the same failure:
goma:error, and an un-namespacederror. The bare one is gone;goma:erroris unchanged and still carries the same{ message, code, component? }detail.Like every GomaPulse event it was
composedand bubbling, so it did not stop at the widget. It crossed the shadow root, crossed the document, and arrived at whatever the host had onwindow.addEventListener('error', …): as aCustomEventwithmessage,errorandfilenameallundefined, since those areErrorEventfields aCustomEventdoes not have. A host with error reporting installed received a malformed error record it could neither read nor attribute.That had been written off as harmless on the grounds that the handler behind it could never fire. It can. Vue begins its
errorCapturedwalk at the throwing component's parent, so theonErrorCapturedboundary each widget installs in its own root component cannot see that component's own errors, and being the root, there is nothing above it to catch them either. Anything raised in a widget's root scope (its setup, its render, its lifecycle hooks, its watchers) skips the boundary and reaches the app-level handler every time. Only errors from child components are caught by the boundary, which is what it is for: those show the in-shadow banner and emitgoma:retry.Hosts listening for
goma:errorneed no change. A host listening for bareerroron a widget element should move togoma:error; it was documented as unreachable, so in practice it delivered nothing until now.
0.29.0
Integration caveats: fixes from a cross-widget review of every shipped widget from an integrator's point of view.
Cross-cutting fixes:
@gomagaming/core'screateWidgetElement()factory, andlive-chat's andmessenger's own element classes, now guard against importing a widget package in a non-browser context (SSR): theHTMLElementsubclass was previously defined at module top level in every widget package, so importing any of them under server-side rendering threw aReferenceErrorimmediately.
Per-package fixes:
favorites: addedsrc/themeTokens.jsand a:hoststylesheet (previously the package had neither, so it silently rendered core's base theme instead of its documented dark-navy/cyan/amber theme).suggested-bet. Fixed a leaked WAMP subscription: switching a mounted widget'sdataSourceaway fromrpc/subscribenever released the open topic.filters-settings: fixed a dead:host{display:inline-block}CSS rule (the shared element factory's inlinedisplay:blockalways won), so the widget always rendered full-width instead of the intended compact inline trigger.game-details: added amin-heightfallback for the inert/no-eventIdstate in an unsized container.betslip-sidebar,my-bets,competition-filter,suggested-bet,events-horizontal,top-competitions,top-competitions-pills,competitions-list,multi-banners,casino-games,sports-navigation-horizontal. CorrectedthemeTokens.jsregistries that had drifted from actual token usage, added missinggoma:retryEvents-table rows, and other small doc/registry corrections.region-filter: added a missingtypeof window !== 'undefined'guard on a window listener.live-chat: correctedcustom-elements.json'smembersarray (missingtheme/configaccessors, and an inaccurate description).sports-domain: corrected a stale code comment that seeded a documentation error in two Missions widgets.
Three fixes (
favorites,game-details,filters-settings) change real rendering and are flagged indocs/guide/integration-caveats.mdas needing human visual verification before release.
0.26.0
Stop publishing source maps and package source.
dist/*.js.mapwas emitted by the shared Vite preset and swept into every tarball byfiles: ["dist/", "src/"], with the original source inlined insourcesContent.src/shipped the same source again, in the clear. Both are one URL away from a browser once a package is served from a CDN, so the preset now builds withsourcemap: falseandfilesno longer carriessrc/.@gomagaming/corekeeps the two paths its export map actually points at:src/styles/main.cssandsrc/types/index.d.ts. Its tarball drops from 1741 files to 576, which also brings it back under the 900-file limit a per-request unpacker can handle.Deep imports of
@gomagaming/<pkg>/src/*from an installed package no longer resolve. Nothing documented used them; imports inside the monorepo are unaffected.
0.9.0
New widget
<goma-competitions-list>: vertical list of competitions grouped by region, scoped per sport.Each region renders as a collapsible card (flag + region name + caret in the header, competition rows with separator lines in the expanded body), matching the design. Multi-open accordion; the first region card is expanded by default, controlled via
initialOpenRegionIds.- Reuses
useCompetitionsSource+useLocationsSource; joins them client-side onvenueIdto build the per-region groups. - Three data sources via
dataSource:'prop'(host-fed viael.competitions/el.regions),'rpc'(one-shotgetCompetitions+getLocationson connect; default),'subscribe'(placeholder, falls back to RPC). sportIdis mandatory (defaults to 1 = Football). Pair with the sports picker viafollowSportPickerto re-fetch on sport change.- Emits
goma:competition-selectwith the same payload as<goma-competition-filter>: downstream<goma-events-horizontal>withfollowCompetitionPicker: truecooperates with no changes. - Also emits
goma:region-toggle(informational) when a region accordion card is opened or closed.
Released alongside the rest of the lockstep family at a minor bump.
- Reuses
New widget
<goma-multi-banners>. A swipeable carousel of injectable banner cards, three types in one widget: sport events, casino game promos, and info banners.All data is host-fed via the
bannersarray (no CMS). For a sport banner, settingeventId(live or prelive) makes the widget resolve teams, start time, score and odds itself; without it the host supplies those fields inline. The background image is always host-fed.- Sport cards adapt to event state: live cards show the running clock/part and score (
home - away), prelive cards show the kickoff date + time. - Carousel chrome: prev/next arrows, pagination dots, swipe/drag, opt-in autoplay (off by default). The track is clipped + isolated so nothing bleeds over neighbouring widgets.
- Emits
goma:banner-select(every banner),goma:event-navigate(sport-card body tap → game-details), andgoma:outcome-select(sport odds tap → betslip, same shape as events-horizontal). - No new theme tokens; all colours map to existing
--goma-*tokens.
@gomagaming/sports-domainadditions (additive, no change to existing paths):- New action group
sports.bannerEvents(createBannerEventsActions): a multi-event subscription manager that accumulates events intobettingStore.events(skipStoreCleanup) so several event cards keyed by id coexist with live odds, unlike the singular-gamematchDetailspath. - Public exports for
useGameScoresTallOddsanduseGamePartsAndTimesTallOddsso other widgets can render live scores/clocks from a store event.
- Sport cards adapt to event state: live cards show the running clock/part and score (
0.8.0
New widget
<goma-top-competitions>: sidebar "Top Competitions" list.A vertical list of competition rows (sport icon + region flag + name) with a section title above. Selecting a row emits
goma:competition-selectwith the full payload (competitionId, competitionName, sportSportId, competitionVenueId, previousCompetitionId).- Three data sources via
dataSource:'prop'(host-fed),'rpc'(one-shotpopularTournamentscall on connect; default),'subscribe'(placeholder, falls back to RPC). - Sport scope set once via
config.sportId; no picker-chain wiring. - Sport icons resolved off
iconBaseUrl(/icons/sports/dark_mode/{sportId}.svg), flags offflagBaseUrl({venueId}.png). Same conventions as the rest of the filter family. - New composable
useTopCompetitionsSourcein@gomagaming/sports-domainwraps the existingoddsApi.betting.getTopCompetitionsRPC.
Released alongside the rest of the lockstep family at a minor bump.
- Three data sources via
0.7.0
Playground scenario presets, live theme-token editor, and family-wide picker-coupling defaults.
Widget runtime defaults
- events-horizontal:
followSportPicker,followRegionPicker,followCompetitionPicker,followTimePickerall flip fromfalsetotruein the element-class defaults and the playground manifest. Drop a sibling picker or filter on the page and the events feed auto-couples without any explicit config. Consumers that don't want the coupling set the flag tofalse(or the matching HTML attributefollow-sport-picker="false", etc.). - region-filter:
followSportPickerdefault flips totruein the element class + manifest. The widget will re-fetch the region list whenever a sibling sports picker emitsgoma:sport-select. - competition-filter:
followSportPickerandfollowRegionPickerdefaults both flip totrue. Drops the manual wiring step from the picker-chain composition. - region-filter / competition-filter: playground manifest's
sourcedefault flips from'mock'to'rpc'. Aligns the manifest with the element class's owndataSource: 'rpc'default; consumers of the published widgets are unaffected (mock was a playground-only convenience).
Breaking-ish note: a host page that mounts multiple of these widgets side-by-side without setting
follow*Pickerexplicitly will now see them auto-react to each other's selection events. The old isolated behaviour is restored withfollow*Picker: false.Playground SPA additions (not published; bundled into the docs site at
/playground)- Scenario presets: a named scenario is an ordered set of widgets
- per-instance configs + theme + token overrides + locale. Three built-ins ship:
full-betting(filters row + sports nav + events + game details + sidebar betslip),mobile-layout(dialog picker + events + floating betslip), anddark-neon-blue-showcase. TopBar gains a Scenario<select>that replaces widgets / configs / theme in one mutation; the dropdown auto-flips to "Custom" the moment the user diverges from the preset.
- per-instance configs + theme + token overrides + locale. Three built-ins ship:
- Live theme-token editor: new Inspector "Theme" tab grouped by prefix (Backgrounds, Text, Icons, Highlights, …). Edits write through
store.setThemeToken('--goma-X', '#hex')to the global override layer (sparseRecord<--goma-key, hex>), which Stage's fast-path watcher pushes viael.theme = …to every mounted element._applyTheme()writeshost.style.setProperty(--goma-X)reactively, so token edits repaint with no Vue remount. - Share URL: v3 envelope additively carries
themeTokensandactiveScenario. Default values are dropped from the encoded payload (size optimisation, hydrate re-applies defaults). Foreign payloads with poisonedthemeTokens(non-object, arrays, non-string values) are normalised back to{}inhydrate(). dark-minttheme renamed todark-neon-blue: palette pulled verbatim from the new Figma tokens export (Uniclub Dark Neon Blue). Highlights / live tags / button-primary flip from mint green (#5cffc0,#00ffbc) to neon cyan (#29f2ff); secondary accent flips from indigo#5b56ffto purple#8750ff. Backgrounds, text, icons, separators, dialog background are unchanged. New tokens added: inputs, alerts, betslip, game header, hero card, tickets, stats, shadows. Persisted state or share URLs that pintheme: 'dark-mint'fall back toTHEMES.defaultafter hydrate.
Tests
- New
tests/playground-scenarios.test.js: 19 vitest cases coveringapplyScenario,setThemeToken+activeScenarioCustom-flip,resetThemeTokens, share URL round-trip with the new envelope fields, hydrate normalisation of malformed payloads. - New
tests/integration/playground-scenarios.spec.ts. 4 Playwright cases: scenario → DOM mount, dark-neon-blue palette assertion (backgroundPrimary+highlightPrimary), live token edit without remount (element identity preserved), share URL round-trip across pages. tests/integration/playground-inspector.spec.ts: tab list extended with the new "Theme" tab.
- events-horizontal:
0.6.0
New widget:
<goma-competition-filter>. Horizontal pill row of competitions (tournaments, leagues) with a hardcoded "All" pill on the leftmost slot, single-selection state, and optional competition icons resolved officonBaseUrl(same convention as sport-navigation,competitions/{id}.svg). Sister widget to<goma-region-filter>; same data-source surface, same picker-chain pattern, plus an additional client-side filter wired to the region picker.What's new
- New package
@gomagaming/competition-filter. Element:<goma-competition-filter>. Reference docs:docs/widgets/competition-filter.md. - Hardcoded "All" pill emits
{ competitionId: 'all', competitionName: <localised> }viagoma:competition-select: maps straight to the matches-aggregator topic's literalallno-filter tournamentId segment. - Opt-in
followSportPicker(re-fetch on sport-select) andfollowRegionPicker(client-side filter onvenueIdvia the picked regionId;'all'disables). Both resetselectedCompetitionIdto'all'and emitgoma:competition-selecton context change so downstream listeners clear their tournamentId filter in the same tick.
@gomagaming/events-horizontaladditions- New
followCompetitionPicker+competitionPickerSelectoropt-in props. Third in the picker chain: listens forgoma:competition-selectat the window and rewrites segment index 2 (tournamentId) of the matches-aggregator topic. The existingrewriteTopicSegment(topic, idx, value)helper does the lifting: adapter-resolved prefix when available, canonical'custom-matches-aggregator/'fallback otherwise (works in mock mode + pre-WAMP-connect). - Playground manifest gains a
followCompetitionPickertoggle under "advanced".
@gomagaming/sports-domainadditionsuseCompetitionsSource(state): public composable. Watchesstate.sportId+state.regionId+state.dataSource; callsoddsApi.betting.getCompetitions({ sportId })on connect and mirrors avenueId-filtered slice ofbettingStore.competitionsintostate.competitions. The region filter runs client-side without requiring a re-fetch.subscribeToCompetitions({ sportId, afterInitialDump })stub on the everymatrix betting module (warn-once + RPC fallback, same shape assubscribeToLocations). Backend topic for live competition updates is deferred.
@gomagaming/coreadditions- Four new bundled i18n keys per locale (en/fr/pt):
competition_filter_all_label,competition_filter_aria,competition_filter_empty,competition_filter_live_events_count(vue-i18n 3-form plural for the live-count badge).
gomawt playground
competition-filterjoins the registry, typesWidgetIdunion, storeALL_WIDGETS, VitePress theme side-effect import, Stage event-log allow-list (goma:competition-select), and EventLogPanel category classifier.- demo entry-file imports added to playground/isolation/stress/ soak entries; workspace dep +
optimizeDeps.excludeentry indemo/.
Three-layer chain ready out of the box: drop a sports navigation picker, a region filter, a competition filter, and an events-horizontal feed on the same page; turn on the relevant
follow*Pickerflags and the topic chain{sportId}/{locationId}/{tournamentId}updates segment-by-segment with no host glue and no WAMP reconnect.- New package
Drop the unused
operatorIdfield from the public widget config surface. The field was declared in@gomagaming/coredefaults + TypeScript types but never read anywhere on the data path:ucsOperatorIdis the only operator identifier the EveryMatrix adapter (anduseBetslipLogic) consume. Settingconfig.operatorIdon a widget element now logs an "unknown config key" warning indebug: truemode instead of being silently accepted.Migration: consumers using
config.operatorIdshould rename toconfig.ucsOperatorId. No behavioural change for consumers who already only setucsOperatorId(which the docs have always recommended).@gomagaming/corechanges:createWidgetElement.js:COMMON_DEFAULTSno longer seedsoperatorId; theknownTopLevelKeysvalidator set drops it.types/index.d.ts: removedoperatorId?: stringfrom theGomaWidgetConfiginterface.
@gomagaming/betslip-sidebardocstring tweak:custom-elements.jsonexample endpoint placeholder changed from/place-bet/4313/v2/betsto/place-bet/2838/v2/bets(matches the staging operator id used by the playground EM demo preset). No behavioural change.
gomawt playground:
- The "Connection" panel drops the
operatorIdinput row; share-URL envelope, persistence, types, and preset filler all scrubbed in lockstep so older share URLs that carriedoperatorIdare silently ignored on hydrate (the field falls out ofSTATE_DEFAULTSand the validator'sstringFieldslist). - New mobile UI: ≤760 px viewport gets a bottom tab bar (Widgets / Stage / Inspect) routing the body panels; the topbar folds Mode/Theme/Locale/API URL behind a
⋮disclosure; Connection panel grid collapses to a single column; tab targets meet 44 px touch-target guidance and inputs use 16 px font to suppress iOS focus-zoom. - EM demo preset refreshed to the NDW staging endpoint (
wss://api-ndwstage.everymatrix.com/v2, realmwww.demo1-stage.everymatrix.com, ucsOperatorId2838).
VitePress docs site:
- Locked to dark mode (
appearance: 'force-dark'): the appearance toggle is hidden from the nav. The playground's dark--bg-*tokens no longer two-tone against a light VitePress chrome. - Homepage, introduction, publishing, and widget reference docs refreshed to current state: nine widgets shipped, eleven publishable packages in the lockstep group, example URLs scrubbed of the previous client-specific staging hostnames (now use generic
api.example.com/sports-api.example.complaceholders), exampleucsOperatorIdliteral updated to2838. Three new widget docs (region/competition/time-filter) added to the homepage feature sections.
New widget:
<goma-region-filter>. Horizontal pill row of regions (countries) with a hardcoded "All" pill on the leftmost slot, single-selection state, and an optional flag glyph per pill. Sources its region list from a one-shot WAMP RPC call on connect, with a placeholdersubscribemode that currently falls back to RPC until the backend exposes a live-update topic for locations.What's new
- New package
@gomagaming/region-filterpublished in lockstep with the existing widgets. Element:<goma-region-filter>. Reference docs:docs/widgets/region-filter.md. - Hardcoded "All" pill emits
{ regionId: 'all', regionName: 'All' }viagoma:region-select(canonical "no filter" sentinel, matches the matches-aggregator topic format wherelocationId='all'). - Flag glyphs rendered from a configurable CDN: default
flagBaseUrlis the upstream Glastcoper flag pack, resolved as${flagBaseUrl}{regionId}.png. Falls back to a globe glyph when a flag fails to load. - Opt-in
followSportPickerautomation (mirrors<goma-events-horizontal>): when enabled, the widget listens at the window forgoma:sport-selectevents emitted by sibling sports-navigation widgets and rewrites itssportIdso the region list refetches automatically. OptionalsportPickerSelectorscopes the listener to a specific picker for multi-instance disambiguation.
@gomagaming/events-horizontaladditions- New
followRegionPicker+regionPickerSelectoropt-in props. Mirror the existingfollowSportPickerplumbing but listen forgoma:region-selectand rewrite the locationId segment (index 1) of the matches-aggregator topic. The region-filter's hardcoded "All" pill emitsregionId: 'all'which lines up exactly with the matches-aggregator topic's literalallno-filter segment: no special-casing needed. Drop a<goma-region-filter>alongside an events-horizontal feed and setfollow-region-pickeron the latter; selections flow through with no host glue and no WAMP reconnect (the existinguseTopicSubscriptionwatcher rebinds the topic in place).
@gomagaming/sports-domainadditionsuseLocationsSource(state): public composable that drivesstate.regionsfrom the WAMP API. Watchesstate.dataSource,state.sportId, and the apiContainer's connection; on connect, callsoddsApi.betting.getLocations({ sportId })and mirrorsbettingStore.locationsinto the widget's reactive state.subscribeToLocations({ sportId, afterInitialDump }): new stub method on the Everymatrix betting module. Logs a one-time warning and falls back to one-shotgetAllLocationsuntil the live topic is wired backend-side. Returns a no-op unregister so the composable's cleanup contract stays uniform with the other subscribe methods.
@gomagaming/coreadditions- Three new bundled i18n keys across
en.json,fr.json, andpt.json:region_filter_aria,region_filter_empty.
gomawt playground
region-filterjoins the playground registry, storeALL_WIDGETS, typesWidgetIdunion, and VitePress theme side-effect imports. The embedded/playgroundroute can mount it alongside the other six widgets.
- New package
New widget:
<goma-time-filter>. Horizontal pill row of time-interval filters with a hardcoded "All" pill on the leftmost slot. Pure host-fed; no WAMP transport, no sport-scoping, no API container.Lighter than the rest of the filter family: no
setupAppplumbing on the element, no shared composable in@gomagaming/sports-domain. Just anintervalsprop, aselectedTimeValueselection state, and agoma:time-selectevent.What's new
- New package
@gomagaming/time-filter. Element:<goma-time-filter>. Reference docs:docs/widgets/time-filter.md. - The host feeds
el.intervals = [{ label, value }, …].labelis rendered verbatim (hosts pre-localise, only the hardcoded "All" pill is locale-driven, via the bundledtime_filter_all_labelkey).valuematches the matches-aggregator topic's hoursInterval segment ('0-1','0-24','24-48', etc.). Malformed entries drop silently; duplicates byvaluekeep the first occurrence. - The hardcoded "All" pill emits
{ timeValue: 'all', timeLabel: <localised>, previousTimeValue }viagoma:time-select: maps straight to the matches-aggregator topic's literalallno-filter hoursInterval segment.
@gomagaming/events-horizontaladditions- New
followTimePicker+timePickerSelectoropt-in props. Fourth in the picker chain. Listens forgoma:time-selectat the window and rewrites segment index 3 (hoursInterval) of the matches-aggregator topic withdetail.timeValue. Reuses the existingrewriteTopicSegment(topic, idx, value)helper (with its canonical-prefix fallback so mock-mode + pre-WAMP-connect rewrites still land). - Playground manifest gains a
followTimePickertoggle under "advanced".
@gomagaming/coreadditions- Three new bundled i18n keys per locale (en/fr/pt):
time_filter_all_label,time_filter_aria,time_filter_empty.
gomawt playground
time-filterjoins the registry, typesWidgetIdunion, storeALL_WIDGETS, VitePress theme side-effect import, Stage event-log allow-list (goma:time-select), and EventLogPanel category classifier.- demo entry-file imports added across playground/isolation/stress/ soak; workspace dep +
optimizeDeps.excludeentry indemo/.
Four-segment chain ready out of the box: drop a sports navigation picker, a region filter, a competition filter, a time filter, and an events-horizontal feed on the same page; turn on the relevant
follow*Pickerflags and the topic chain{sportId}/{locationId}/{tournamentId}/{hoursInterval}updates segment-by-segment with no host glue and no WAMP reconnect.- New package
All notable changes to this package will be documented in this file. Auto-generated by Changesets; do not edit by hand.