goma-competition-filter
Horizontal pill row of competitions (tournaments, leagues) with a hardcoded "All" pill on the left, single-selection state, and optional competition icons resolved off iconBaseUrl. Sister widget to <goma-region-filter>; both follow the same picker-chain pattern as the sports navigation widgets.
Live demo
Open in the playground → — toggle Competition filter in the widget rail, then enable Follow sport picker and Follow region picker alongside <goma-sports-navigation-horizontal> and <goma-region-filter> to see the three-layer chain in action.
Install
@gomagaming/competition-filter is a scoped private package. See Installation for the one-time .npmrc setup.
bash
npm install @gomagaming/competition-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-competition-filter id="cf"></goma-competition-filter>Configuration
js
const el = document.getElementById('cf')
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',
iconBaseUrl: '/dist',
}| Key | Type | Required | Description |
|---|---|---|---|
socketUrl | string | Yes | WAMP socket URL. The widget hits /sports#tournaments 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 URLs. |
locale | string | No | i18n locale. Defaults to 'en'. |
iconBaseUrl | string | No | Base URL where competition icons are served from. Resolves to ${iconBaseUrl}/icons/competitions/{id}.svg. |
theme | object | No | CSS variable overrides — see Theming. |
messages | object | No | i18n overrides per locale. |
debug | boolean | No | Verbose console logging. |
Data sources
'rpc' — one-shot WAMP call (default)
The widget calls apiSession.$wcall('/sports#tournaments', { sportId, lang, liveStatus: 'BOTH', sortByPopularity: true }) once the WAMP connection is up. 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 competition updates is not yet available. The widget accepts
dataSource: 'subscribe'but internally falls back to the one-shot RPC and logs a single console warning. Once the topic ships, the fallback flips automatically.
Props (HTML attributes + JS properties)
| Property | Attribute | Default | Description |
|---|---|---|---|
dataSource | data-source | 'rpc' | 'rpc' / 'subscribe'. |
sportId | sport-id | 1 | Sport id passed to the locations RPC. |
regionId | region-id | 'all' | Client-side filter on venueId. 'all' disables. Auto-set by followRegionPicker. |
competitions | — | [] | Array of GomaCompetition objects. Populated by the WAMP composable. |
selectedCompetitionId | selected-competition-id | 'all' | Id of the currently-selected pill. Two-way: clicks update this value. |
showLabels | show-labels | true | Render the label on each pill (uses `shortTranslatedName |
showIcons | show-icons | true | Render the competition icon (or fallback trophy glyph). |
spaceBetween | space-between | 8 | Pixel gap between pills. |
skeletonCount | skeleton-count | 8 | Number of placeholder pills. |
followSportPicker | follow-sport-picker | false | Listen for goma:sport-select and re-fetch the list. |
sportPickerSelector | sport-picker-selector | null | Scope followSportPicker to a specific picker. |
followRegionPicker | follow-region-picker | false | Listen for goma:region-select and client-side filter the list. |
regionPickerSelector | region-picker-selector | null | Scope followRegionPicker to a specific filter. |
GomaCompetition shape
ts
interface GomaCompetition {
id: number
name: string
translatedName?: string
shortTranslatedName?: string // preferred display (falls back to translatedName → name)
sportId: number
venueId?: number // used by followRegionPicker's client-side filter
numberOfEvents?: number
numberOfLiveEvents?: number // > 0 → live-count badge
isTopLevelTournament?: boolean
}The hardcoded "All" pill
Always rendered as the leftmost pill. Clicking it emits:
js
{
competitionId: 'all',
competitionName: 'All', // locale-aware
competitionSportId: null,
competitionVenueId: null,
previousCompetitionId: <whatever was selected>,
}Hosts treat competitionId === 'all' as the canonical "no filter" sentinel — maps straight to the matches-aggregator topic's literal all tournamentId segment.
followSportPicker
js
el.followSportPicker = true
el.sportPickerSelector = 'goma-sports-navigation-horizontal#main' // optionalWhen enabled, listens at the window for goma:sport-select and rewrites state.sportId → useCompetitionsSource re-runs the RPC for the picked sport. Also resets selectedCompetitionId to 'all' and emits goma:competition-select so a downstream <goma-events-horizontal> clears its tournamentId filter.
followRegionPicker
js
el.followRegionPicker = true
el.regionPickerSelector = 'goma-region-filter#regions' // optionalWhen enabled, listens at the window for goma:region-select and stashes regionId on state. useCompetitionsSource filters the mirrored list client-side to entries whose venueId matches the picked regionId; 'all' disables the filter. No new RPC roundtrip — purely a UI refinement of the already-fetched list. Same cross-context reset as the sport-picker hook.
Sort order
Competitions with numberOfLiveEvents > 0 float to the top (desc by live count); the rest preserve feed order at the tail. "All" pill is always first.
Events
| Status | Event | Detail | Emitted when |
|---|---|---|---|
| Canonical | ready | {} | Widget mounted. |
| Canonical | goma:connection | { state, … } | Connection state transition. |
| Canonical | goma:error | { message, code, component? } | Render or data error. |
| Canonical | goma:competition-select | { competitionId, competitionName, competitionSportId, competitionVenueId, previousCompetitionId } | User taps a competition pill or the "All" pill. Also emitted when followSportPicker / followRegionPicker triggers a context-change reset. |
Embedding examples
Vanilla HTML — three-layer chain
html
<goma-sports-navigation-horizontal id="sports"></goma-sports-navigation-horizontal>
<goma-region-filter id="regions" follow-sport-picker></goma-region-filter>
<goma-competition-filter
id="comps"
follow-sport-picker
follow-region-picker
></goma-competition-filter>
<goma-events-horizontal
id="feed"
follow-sport-picker
follow-region-picker
follow-competition-picker
topic="custom-matches-aggregator/1/all/all/all/POPULAR/LIVE/10/5"
></goma-events-horizontal>
<script type="module">
import '@gomagaming/sports-navigation-horizontal'
import '@gomagaming/region-filter'
import '@gomagaming/competition-filter'
import '@gomagaming/events-horizontal'
const sharedConfig = { /* socket + REST creds */ }
for (const id of ['sports', 'regions', 'comps', 'feed']) {
document.getElementById(id).config = sharedConfig
}
</script>Tap a sport → all three sub-widgets react. Tap a region → competition list + events feed react. Tap a competition → events feed re-binds its topic. No host glue, no WAMP reconnects.
React
jsx
import '@gomagaming/competition-filter'
import { useEffect, useRef } from 'react'
function CompetitionFilter({ config, sportId = 1 }) {
const ref = useRef(null)
useEffect(() => {
if (!ref.current) return
ref.current.dataSource = 'rpc'
ref.current.sportId = sportId
ref.current.followRegionPicker = true
ref.current.config = config
}, [config, sportId])
return (
<goma-competition-filter
ref={ref}
onGoma:competition-select={(e) => console.log(e.detail)}
/>
)
}Vue 3
vue
<script setup>
import '@gomagaming/competition-filter'
import { ref, onMounted } from 'vue'
const el = ref(null)
const props = defineProps(['config'])
onMounted(() => {
el.value.dataSource = 'rpc'
el.value.followSportPicker = true
el.value.followRegionPicker = true
el.value.config = props.config
})
</script>
<template>
<goma-competition-filter ref="el" @goma:competition-select="onCompetitionSelect" />
</template>Theming
Same --goma-* tokens as goma-region-filter. Override via el.theme = { … }.
| Token | Element |
|---|---|
--goma-backgroundPrimary | Rail background |
--goma-backgroundCards | Inactive pill background |
--goma-textPrimary | Inactive pill label |
--goma-textSecondary | Icon fallback glyph |
--goma-separatorLine | Icon ring |
--goma-highlightPrimary | Selected pill background |
--goma-highlightPrimaryContrast | Selected pill label |
--goma-liveCountBackground | Live-count badge background |
--goma-liveCountText | Live-count badge text |
Accessibility
Same a11y contract as <goma-region-filter>: role="tablist" swiper, role="tab" + tabindex="0" + aria-selected on every pill, keyboard parity (Enter / Space), localised aria-labels for the rail and the live-count badge.
Source layout
| Layer | File |
|---|---|
| Element class | packages/competition-filter/src/CompetitionFilterElement.js |
| App wrapper | packages/competition-filter/src/CompetitionFilterApp.vue |
| Component tree | packages/competition-filter/src/components/CompetitionFilter/{CompetitionList,CompetitionItem}.vue |
| Source composable | packages/sports-domain/src/composables/ui/useCompetitionsSource.js |
| Competitions RPC + subscribe stub | packages/sports-domain/src/api/everymatrix/modules/betting.js |
| Manifest | packages/competition-filter/custom-elements.json |
What's deferred
- Real subscribe path. Backend topic for live competition updates not yet known;
subscribeToCompetitionsfalls back to one-shot RPC + warn-once until then. - Region-aware RPC.
getCompetitions({ sportId })doesn't accept a regionId; the followRegionPicker filter runs client-side. If a future RPC supports region scoping, the composable can switch to a server-side filter transparently.