Skip to content

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-filter

You 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-toastification

Element

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',
}
KeyTypeRequiredDescription
socketUrlstringYesWAMP socket URL. The widget hits /sports#locations over this transport.
socketRealmstringYesWAMP realm.
apiBaseUrlstringYesREST base URL.
bettingApiBaseUrlstringNoREST base URL for betting offers.
ucsOperatorIdstringYesOperator ID — used to build per-tenant subscription URLs.
localestringNoi18n locale. Defaults to 'en'.
themeobjectNoCSS variable overrides — see Theming.
messagesobjectNoi18n overrides per locale.
debugbooleanNoVerbose 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)

PropertyAttributeDefaultDescription
dataSourcedata-source'rpc''rpc' / 'subscribe'. See Data sources.
sportIdsport-id1Sport 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.
selectedRegionIdselected-region-id'all'Id of the currently-selected pill. 'all' selects the hardcoded sentinel. Two-way: a click also updates this value.
showLabelsshow-labelstrueRender the short label on each pill.
showFlagsshow-flagstrueRender the flag glyph on each region pill. The "All" pill never renders a flag.
spaceBetweenspace-between8Pixel gap between pills.
skeletonCountskeleton-count8Number of placeholder pills shown until the first regions assignment.
flagBaseUrlflag-base-urlhttps://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.
followSportPickerfollow-sport-pickerfalseWhen true, listen at the window for goma:sport-select and rewrite sportId. See followSportPicker.
sportPickerSelectorsport-picker-selectornullCSS 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'  // optional

Without 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

  1. Skeleton — pulsing placeholder pills render alongside the "All" pill from mount until the first regions assignment. Count is configurable via skeletonCount.
  2. Empty — once an empty array arrives, only the "All" pill is shown.
  3. 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

StatusEventDetailEmitted when
Canonicalready{}Widget mounted, initial render done.
Canonicalgoma:connection{ state, attempt?, nextRetryMs?, lastError?, attempts? }Connection state transition.
Canonicalgoma:error{ message, code, component? }Render or data error.
Canonicalgoma: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.
AliaserrorSame as goma:errorDispatched 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

TokenElement
--goma-backgroundPrimaryRail background
--goma-backgroundCardsInactive pill background
--goma-textPrimaryInactive pill label
--goma-textSecondaryFlag fallback glyph
--goma-separatorLineFlag-circle ring
--goma-highlightPrimarySelected pill background
--goma-highlightPrimaryContrastSelected pill label
--goma-liveCountBackgroundLive-count badge background
--goma-liveCountTextLive-count badge text

See the Theming guide for the complete token reference.

Accessibility

ElementRoleAttributes
Swiper containertablistaria-label="Region filter" (i18n key region_filter_aria)
Region pilltabtabindex="0", aria-selected, aria-label="{name}"
Live-count badgearia-label="N live event(s)"
Error bannerrole="alert" for screen-reader announcement

Keyboard: Tab to focus pills, Enter / Space to select. :focus-visible ring uses the --goma-highlightPrimary token.

Source layout

LayerFile
Element classpackages/region-filter/src/RegionFilterElement.js
App wrapperpackages/region-filter/src/RegionFilterApp.vue
Component treepackages/region-filter/src/components/RegionFilter/*
Entry / registrationpackages/region-filter/src/index.js
Manifestpackages/region-filter/custom-elements.json

Shared pieces live in @gomagaming/sports-domain:

LayerFile
Locations RPC + subscribe stubpackages/sports-domain/src/api/everymatrix/modules/betting.js
Source composablepackages/sports-domain/src/composables/ui/useLocationsSource.js
Location mapperpackages/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, subscribeToLocations will be wired through and the one-time fallback warning will disappear. The public surface (the dataSource: '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.