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-listYou 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-toastificationElement
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',
}| Key | Type | Required | Description |
|---|---|---|---|
socketUrl | string | When dataSource !== 'prop' | WAMP socket URL. The widget hits getCompetitions + getLocations over this transport. |
socketRealm | string | When dataSource !== 'prop' | WAMP realm. |
apiBaseUrl | string | When dataSource !== 'prop' | REST base URL. |
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. |
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)
| Property | Attribute | Default | Description |
|---|---|---|---|
sportId | sport-id | 1 | Sport 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'). |
selectedCompetitionId | selected-competition-id | null | Currently-selected row id. Two-way: clicks update it. |
dataSource | data-source | 'rpc' | 'prop' | 'rpc' | 'subscribe'. |
showTitle | show-title | true | Render the "Competitions" header above the list. |
skeletonCount | skeleton-count | 4 | Placeholder cards before first non-empty data. |
flagBaseUrl | flag-base-url | https://static.glastcoper.com/.../flag/ | Country flag CDN base. Flags resolve to ${flagBaseUrl}{venueId}.png. |
initialOpenRegionIds | initial-open-region-ids | 'first' | Initial expanded state: 'first' | 'all' | 'none' | Array<id>. |
followSportPicker | follow-sport-picker | true | Listen 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. |
sportPickerSelector | sport-picker-selector | null | Optional 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
| Name | Detail | Notes |
|---|---|---|
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.
| Token | Default | Used for |
|---|---|---|
--goma-backgroundPrimary | #03061b | Outer container background |
--goma-backgroundCards | #1f2147 | Region card background |
--goma-textPrimary | #ffffff | Region + competition labels |
--goma-textSecondary | #939dff | Empty-state + flag fallback |
--goma-iconPrimary | #ffffff | Caret stroke |
--goma-separatorLineSecondary | #393d83 | Row separator line |
--goma-highlightPrimary | #ff6600 | Focus ring |
Accessibility
- Each region header is a
<button>witharia-expandedandaria-controlspointing at the body it toggles. - The body region renders with
role="region"andaria-label="{region name}". - Competition rows have
role="button",tabindex="0", and respond to Enter / Space in addition to mouse clicks. Selection setsaria-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
| Layer | File |
|---|---|
| Element class | packages/competitions-list/src/CompetitionsListElement.js |
| App wrapper | packages/competitions-list/src/CompetitionsListApp.vue |
| List + grouping | packages/competitions-list/src/components/CompetitionsList/CompetitionsList.vue |
| Region card | packages/competitions-list/src/components/CompetitionsList/RegionAccordionCard.vue |
| Competition row | packages/competitions-list/src/components/CompetitionsList/CompetitionRow.vue |
| Competitions source | packages/sports-domain/src/composables/ui/useCompetitionsSource.js |
| Locations source | packages/sports-domain/src/composables/ui/useLocationsSource.js |
| Tests | packages/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.