Skip to content

goma-competitions-list

Vertical list of competitions grouped by region — one collapsible card per region with a flag, region name, and caret in the header, plus a body of competition rows separated by dividers. Sport-scoped. Pairs with the sport picker via followSportPicker, and emits goma:competition-select (same payload as <goma-competition-filter>) so a downstream <goma-events-horizontal> with followCompetitionPicker: true cooperates with no changes.

Live demo

Open in the playground → — toggle Competitions list in the widget rail.

Install

@gomagaming/competitions-list is a scoped private package. See Installation for the one-time .npmrc setup.

bash
npm install @gomagaming/competitions-list

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

Element

html
<goma-competitions-list id="comps" sport-id="1"></goma-competitions-list>

Configuration

js
const el = document.getElementById('comps')
el.config = {
  socketUrl: 'wss://sportsapi.example.com/v2',
  socketRealm: 'www.example.com',
  apiBaseUrl: 'https://api.example.com',
  locale: 'en',
}
KeyTypeRequiredDescription
socketUrlstringWhen dataSource !== 'prop'WAMP socket URL. The widget hits getCompetitions + getLocations over this transport.
socketRealmstringWhen dataSource !== 'prop'WAMP realm.
apiBaseUrlstringWhen dataSource !== 'prop'REST base URL.
localestringNoi18n locale. Defaults to 'en'.
themeobjectNoCSS variable overrides — see Theming.
messagesobjectNoi18n overrides per locale.
debugbooleanNoVerbose console logging.

This widget is read-only (no betting) — it does not consume the betting-only bettingApiBaseUrl / ucsOperatorId config keys.

Data sources

'prop' — host-fed

The widget renders whatever the host writes to el.competitions and el.regions. Competitions are grouped by venueId; the matching entry in el.regions supplies the canonical region label (falls back to the venueName carried on the competition record when absent).

js
el.dataSource = 'prop'
el.regions = [
  { id: 1, name: 'International - Clubs', translatedName: 'International - Clubs' },
  { id: 41, name: 'France', translatedName: 'France' },
]
el.competitions = [
  { id: 106, name: 'Champions League', sportId: 1, venueId: 1 },
  { id: 107, name: 'Europe League',    sportId: 1, venueId: 1 },
  { id: 301, name: 'Ligue 1',          sportId: 1, venueId: 41 },
]

'rpc' — one-shot WAMP call (default)

The widget calls getCompetitions({ sportId }) and getLocations({ sportId }) once the WAMP connection is up, then joins them client-side. Re-runs whenever sportId changes.

js
el.dataSource = 'rpc'
el.sportId = 1
el.config = { /* socket + REST creds */ }

'subscribe' — live WAMP subscription (placeholder)

Note: the backend topics for live competitions / locations updates are not yet wired. The widget accepts dataSource: 'subscribe' but internally falls back to the one-shot RPCs (warn-once). Once the topics ship, the fallback flips automatically.

Props (HTML attributes + JS properties)

PropertyAttributeDefaultDescription
sportIdsport-id1Sport id passed to the RPCs. Mandatory — the backend endpoints are sport-scoped.
competitions[]Host-fed competitions array (when dataSource: 'prop').
regions[]Host-fed regions array (when dataSource: 'prop').
selectedCompetitionIdselected-competition-idnullCurrently-selected row id. Two-way: clicks update it.
dataSourcedata-source'rpc''prop' | 'rpc' | 'subscribe'.
showTitleshow-titletrueRender the "Competitions" header above the list.
skeletonCountskeleton-count4Placeholder cards before first non-empty data.
flagBaseUrlflag-base-urlhttps://static.glastcoper.com/.../flag/Country flag CDN base. Flags resolve to ${flagBaseUrl}{venueId}.png.
initialOpenRegionIdsinitial-open-region-ids'first'Initial expanded state: 'first' | 'all' | 'none' | Array<id>.
followSportPickerfollow-sport-pickertrueListen at window for goma:sport-select and rewrite sportId. On by default — the widget is sport-scoped and the most common page layout pairs it with a picker.
sportPickerSelectorsport-picker-selectornullOptional CSS selector scoping follow-sport-picker to one element.

GomaCompetition shape

ts
type GomaCompetition = {
  id: string | number
  name: string
  translatedName?: string
  shortTranslatedName?: string
  sportId?: number
  sportName?: string
  venueId: number          // region id — required for grouping
  venueName?: string       // fallback region label if regions array is missing
}

GomaRegion shape

ts
type GomaRegion = {
  id: number
  name: string
  translatedName?: string
}

Events

NameDetailNotes
ready{}Mounted + initial render complete.
error{ message, code, component? }Caught by the widget's error boundary.
goma:competition-select{ competitionId, competitionName, competitionSportId, competitionVenueId, previousCompetitionId }User tapped a competition row. Identical payload to <goma-competition-filter> — downstream <goma-events-horizontal> with followCompetitionPicker: true cooperates with no changes.
goma:region-toggle{ regionId, regionName, isOpen }User toggled a region card. Informational.

All custom events are bubbles: true, composed: true so they cross the Shadow DOM boundary.

Embedding examples

Vanilla HTML

html
<goma-competitions-list id="comps" sport-id="1"></goma-competitions-list>

<script type="module">
  import '@gomagaming/competitions-list'

  const el = document.getElementById('comps')
  el.config = {
    socketUrl: 'wss://sportsapi.example.com/v2',
    socketRealm: 'www.example.com',
    apiBaseUrl: 'https://api.example.com',
    locale: 'en',
  }
  el.addEventListener('goma:competition-select', (e) => {
    console.log('selected', e.detail)
  })
</script>

React

jsx
import { useEffect, useRef } from 'react'
import '@gomagaming/competitions-list'

export function CompetitionsList({ sportId, config }) {
  const ref = useRef(null)
  useEffect(() => {
    if (!ref.current) return
    ref.current.config = config
    ref.current.sportId = sportId
  }, [config, sportId])
  return <goma-competitions-list ref={ref} />
}

Vue 3

vue
<template>
  <goma-competitions-list ref="el" :sport-id="sportId" />
</template>

<script setup>
import { onMounted, ref, watch } from 'vue'
import '@gomagaming/competitions-list'

const el = ref(null)
const props = defineProps(['sportId', 'config'])

onMounted(() => { if (el.value) el.value.config = props.config })
watch(() => props.config, (next) => { if (el.value) el.value.config = next })
</script>

Picker chain — paired with the sport picker

Drop a <goma-sports-navigation-horizontal> (or <goma-sports-navigation-dialog>) anywhere on the page and set followSportPicker on the competitions list to have it re-fetch as the sport changes:

html
<goma-sports-navigation-horizontal id="picker" selected-sport-id="1">
</goma-sports-navigation-horizontal>

<goma-competitions-list
  sport-id="1"
  follow-sport-picker
></goma-competitions-list>

When a picker tab is tapped, the widget rewrites sportId and both source composables re-run their RPCs against the new sport. The first region card auto-expands again under the default initialOpenRegionIds: 'first'.

To scope the listener to ONE specific picker (e.g. when multiple pickers are on the page), set sport-picker-selector:

html
<goma-competitions-list
  sport-id="1"
  follow-sport-picker
  sport-picker-selector="goma-sports-navigation-horizontal#picker"
></goma-competitions-list>

Theming

The widget paints with --goma-* CSS variables. Override via el.theme = { … } or the host page's CSS. See Theming for the full token catalogue.

TokenDefaultUsed for
--goma-backgroundPrimary#03061bOuter container background
--goma-backgroundCards#1f2147Region card background
--goma-textPrimary#ffffffRegion + competition labels
--goma-textSecondary#939dffEmpty-state + flag fallback
--goma-iconPrimary#ffffffCaret stroke
--goma-separatorLineSecondary#393d83Row separator line
--goma-highlightPrimary#ff6600Focus ring

Accessibility

  • Each region header is a <button> with aria-expanded and aria-controls pointing at the body it toggles.
  • The body region renders with role="region" and aria-label="{region name}".
  • Competition rows have role="button", tabindex="0", and respond to Enter / Space in addition to mouse clicks. Selection sets aria-current="true".
  • The expand / collapse button announces its action via a parametrised i18n string (competitions_list_region_expand_aria / competitions_list_region_collapse_aria).

Source layout

LayerFile
Element classpackages/competitions-list/src/CompetitionsListElement.js
App wrapperpackages/competitions-list/src/CompetitionsListApp.vue
List + groupingpackages/competitions-list/src/components/CompetitionsList/CompetitionsList.vue
Region cardpackages/competitions-list/src/components/CompetitionsList/RegionAccordionCard.vue
Competition rowpackages/competitions-list/src/components/CompetitionsList/CompetitionRow.vue
Competitions sourcepackages/sports-domain/src/composables/ui/useCompetitionsSource.js
Locations sourcepackages/sports-domain/src/composables/ui/useLocationsSource.js
Testspackages/competitions-list/tests/CompetitionsList.test.js

What's deferred

  • Live updates. dataSource: 'subscribe' falls back to one-shot RPCs until backend topics for competitions / locations are wired.
  • Drag-to-reorder regions. Region order today is fixed by the locations RPC response.
  • Per-region competition counts. The design ships labels only; counts can be added when the data carries them.