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. Sources its region list from a one-shot WAMP RPC call or a live subscription (placeholder — currently RPC under the hood; see Data sources).
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 surfaces a small set of regions with drifting live counts so you can verify the pill UX without a real WAMP feed.
Install
@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 swiper vue-toastificationElement
html
<goma-region-filter id="rf"></goma-region-filter>Configuration
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',
apiBaseUrl: 'https://api.example.com',
bettingApiBaseUrl: 'https://sports-api.example.com',
ucsOperatorId: '2838',
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. |
apiBaseUrl | string | Yes | REST base URL. |
bettingApiBaseUrl | string | No | REST base URL for betting offers. |
ucsOperatorId | string | Yes | Operator ID — used to build per-tenant subscription URLs. |
locale | string | No | i18n locale. Defaults to 'en'. |
theme | object | No | CSS variable overrides — see Theming. |
messages | object | No | i18n overrides per locale. |
debug | boolean | No | Verbose console logging. |
Data sources
The dataSource property selects how the region list is fetched. The hardcoded "All" pill is always present regardless of mode.
'rpc' — one-shot WAMP call (default)
The widget calls apiSession.$wcall('/sports#locations', { sportId, lang }) once the WAMP connection is up, populates its internal store, then renders. Re-runs whenever sportId changes (including via followSportPicker).
js
el.dataSource = 'rpc'
el.sportId = 1
el.config = { /* socket + REST 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 single console warning. Once the topic ships, this falls back automatically.
js
el.dataSource = 'subscribe'
el.sportId = 1
el.config = { /* socket + REST creds */ }Props (HTML attributes + JS properties)
| 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. Populated by the WAMP composable; rarely set 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. |
followSportPicker | follow-sport-picker | false | When true, listen at the window for goma:sport-select and rewrite sportId. See followSportPicker. |
sportPickerSelector | sport-picker-selector | null | CSS selector that scopes followSportPicker to a specific picker on the page (multi-instance disambiguation). |
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
numberOfLiveEvents?: number // > 0 → renders the live-count badge
numberOfMarkets?: number
numberOfBettingOffers?: number
numberOfLiveMarkets?: number
numberOfLiveBettingOffers?: number
numberOfUpcomingMatches?: number
contexts?: { live?: 1, popular?: 1 }
}The locations endpoint produces these via the shared mapLocation() mapper. The pill UI only reads id, name, shortName, code, and numberOfLiveEvents.
The hardcoded "All" pill
Always rendered as the leftmost pill, never sourced from the API. Clicking it emits:
js
{
regionId: 'all',
regionName: 'All',
regionShortName: 'All',
regionCode: null,
previousRegionId: <whatever was selected>,
}Hosts treat regionId === 'all' (or 'all' as a string) as the canonical "no filter" sentinel. This aligns with the matches-aggregator topic format where locationId = 'all' is the no-filter value.
followSportPicker
Sister widgets in the family emit goma:sport-select events (composed: true, bubbles past Shadow DOM). When followSportPicker is enabled, the region filter listens at the window for those events and rewrites its sportId so the region list refetches for the picked sport.
js
el.followSportPicker = true
el.sportPickerSelector = 'goma-sports-navigation-horizontal#main-picker' // optionalWithout a sportPickerSelector, the filter responds to the first picker on the page. Set the selector when multiple pickers coexist and only one should drive the filter.
Sort order
Regions with numberOfLiveEvents > 0 float to the top of the list (sorted descending by live count); the rest preserve their original feed order at the tail. The "All" pill is always prepended ahead of both groups.
Loading & empty states
- Skeleton — pulsing placeholder pills render alongside the "All" pill from mount until the first regions assignment. Count is configurable via
skeletonCount. - Empty — once an empty array arrives, only the "All" pill is shown.
- Error — render errors surface in a themed banner for 8 seconds with a Retry button.
Flag icons
Each region pill renders an <img> whose src is ${flagBaseUrl}{regionId}.png. The default base URL points at the shared CDN; override flagBaseUrl if you host your own flag pack. If a flag fails to load (404, network error), the pill falls back to a globe glyph automatically. The "All" pill never renders a flag.
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? } | Render or data error. |
| Canonical | goma:region-select | { regionId, regionName, regionShortName, regionCode, previousRegionId } | User taps (or activates via Enter/Space) a region pill — or the "All" pill. The widget also mutates selectedRegionId so the active state flips immediately. |
| Alias | error | Same as goma:error | Dispatched in parallel with goma:error. |
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',
apiBaseUrl: 'https://api.example.com',
ucsOperatorId: '2838',
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(() => {
if (!ref.current) return
ref.current.dataSource = 'rpc'
ref.current.sportId = sportId
ref.current.config = config
}, [config, sportId])
return (
<goma-region-filter
ref={ref}
onGoma:region-select={(e) => console.log(e.detail)}
/>
)
}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 + REST creds */ }
picker.config = sharedConfig
picker.dataSource = 'subscribe'
regions.config = sharedConfig
regions.followSportPicker = true // picks up sport-select events from the picker
</script>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 | Live-count badge background |
--goma-liveCountText | Live-count badge text |
See the Theming guide for the complete token reference.
Accessibility
| Element | Role | Attributes |
|---|---|---|
| Swiper container | tablist | aria-label="Region filter" (i18n key region_filter_aria) |
| Region pill | tab | tabindex="0", aria-selected, aria-label="{name}" |
| Live-count badge | — | aria-label="N live event(s)" |
| 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.
Source layout
| Layer | File |
|---|---|
| Element class | packages/region-filter/src/RegionFilterElement.js |
| App wrapper | packages/region-filter/src/RegionFilterApp.vue |
| Component tree | packages/region-filter/src/components/RegionFilter/* |
| Entry / registration | packages/region-filter/src/index.js |
| Manifest | packages/region-filter/custom-elements.json |
Shared pieces live in @gomagaming/sports-domain:
| Layer | File |
|---|---|
| Locations RPC + subscribe stub | packages/sports-domain/src/api/everymatrix/modules/betting.js |
| Source composable | packages/sports-domain/src/composables/ui/useLocationsSource.js |
| Location mapper | packages/sports-domain/src/api/everymatrix/mappers/sports.mapper.js |
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. - Selection styling parity with the reference design. The reference shows a "Popular" pill alongside "All"; for v1 only "All" is hardcoded. Adding a second sentinel pill is a small follow-up if the design calls for it.