Skip to content

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-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-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',
}
KeyTypeRequiredDescription
socketUrlstringYesWAMP socket URL. The widget hits /sports#tournaments over this transport.
socketRealmstringYesWAMP realm.
apiBaseUrlstringYesREST base URL.
bettingApiBaseUrlstringNoREST base URL for betting offers.
ucsOperatorIdstringYesOperator ID — used to build per-tenant URLs.
localestringNoi18n locale. Defaults to 'en'.
iconBaseUrlstringNoBase URL where competition icons are served from. Resolves to ${iconBaseUrl}/icons/competitions/{id}.svg.
themeobjectNoCSS variable overrides — see Theming.
messagesobjectNoi18n overrides per locale.
debugbooleanNoVerbose 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)

PropertyAttributeDefaultDescription
dataSourcedata-source'rpc''rpc' / 'subscribe'.
sportIdsport-id1Sport id passed to the locations RPC.
regionIdregion-id'all'Client-side filter on venueId. 'all' disables. Auto-set by followRegionPicker.
competitions[]Array of GomaCompetition objects. Populated by the WAMP composable.
selectedCompetitionIdselected-competition-id'all'Id of the currently-selected pill. Two-way: clicks update this value.
showLabelsshow-labelstrueRender the label on each pill (uses `shortTranslatedName
showIconsshow-iconstrueRender the competition icon (or fallback trophy glyph).
spaceBetweenspace-between8Pixel gap between pills.
skeletonCountskeleton-count8Number of placeholder pills.
followSportPickerfollow-sport-pickerfalseListen for goma:sport-select and re-fetch the list.
sportPickerSelectorsport-picker-selectornullScope followSportPicker to a specific picker.
followRegionPickerfollow-region-pickerfalseListen for goma:region-select and client-side filter the list.
regionPickerSelectorregion-picker-selectornullScope 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'  // optional

When enabled, listens at the window for goma:sport-select and rewrites state.sportIduseCompetitionsSource 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'  // optional

When 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

StatusEventDetailEmitted when
Canonicalready{}Widget mounted.
Canonicalgoma:connection{ state, … }Connection state transition.
Canonicalgoma:error{ message, code, component? }Render or data error.
Canonicalgoma: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 = { … }.

TokenElement
--goma-backgroundPrimaryRail background
--goma-backgroundCardsInactive pill background
--goma-textPrimaryInactive pill label
--goma-textSecondaryIcon fallback glyph
--goma-separatorLineIcon ring
--goma-highlightPrimarySelected pill background
--goma-highlightPrimaryContrastSelected pill label
--goma-liveCountBackgroundLive-count badge background
--goma-liveCountTextLive-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

LayerFile
Element classpackages/competition-filter/src/CompetitionFilterElement.js
App wrapperpackages/competition-filter/src/CompetitionFilterApp.vue
Component treepackages/competition-filter/src/components/CompetitionFilter/{CompetitionList,CompetitionItem}.vue
Source composablepackages/sports-domain/src/composables/ui/useCompetitionsSource.js
Competitions RPC + subscribe stubpackages/sports-domain/src/api/everymatrix/modules/betting.js
Manifestpackages/competition-filter/custom-elements.json

What's deferred

  • Real subscribe path. Backend topic for live competition updates not yet known; subscribeToCompetitions falls 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.