Skip to content

goma-game-details

Full details for a single sporting event: header (teams, score, clock), market-category tabs, and a lazy-loaded list of collapsible market cards. Page-shaped — fills its parent. The host owns presentation context (their own route slot, drawer, modal, or sized column); <goma-game-details> owns contents.

Drop a <goma-events-horizontal> and a <goma-game-details> on the same page and card taps drive the details widget out of the box — goma:event-navigate is already emitted by every card today.

Live demo

Open in the playground → — toggle Game details in the widget rail and pick the mock source in the inspector to render a self-contained game with drifting odds and a live clock. Mount Events horizontal alongside it to verify the goma:event-navigate hand-off — tapping a card swaps the details view.

Install

bash
pnpm add @gomagaming/game-details vue pinia vue-router vue-i18n @vueuse/core

Element

html
<goma-game-details id="gd" event-id="12345"></goma-game-details>

The widget fills its parent (:host { display: block; width: 100% }). Give it a sized container (route slot, drawer, sidebar column, modal body — your call).

Configuration

js
const el = document.getElementById('gd')
el.config = {
  // Required — the WAMP server hosting matches + market data
  apiBaseUrl: 'wss://sports-api.example.com',

  // The event to render. Equivalent to setting el.eventId directly.
  eventId: '12345',

  // Optional — locale propagates to bundled i18n
  locale: 'en',

  // Cross-widget cooperation: when true (default), listen on the window
  // for sibling card widgets' `goma:event-navigate` events and patch
  // eventId. Drop-in cooperation with <goma-events-horizontal>.
  followEventNavigate: true,

  // Batching for the market list — IntersectionObserver-driven
  initialBatchSize: 5,        // markets rendered before first scroll
  batchSize: 10,              // markets added per sentinel hit
  intersectionRootMargin: '200px',

  // Surface toggles
  showBackButton: true,
  showCompetition: true,
  showLiveClock: true,
}

The only required key is apiBaseUrl. The widget is inert until eventId is set (either directly or via the watcher).

How the widget loads data

Three subscriptions, each owned by the component it serves:

SubscriptionOwnerFires on
subscribeToMatch(eventId)GameDetailsHeadereventId change
subscribeToMarketGroups(eventId)GameDetailsHeadereventId change
subscribeToMarketsByMarketGroupKey({ matchId, groupKey })GameDetailsMarketsList(eventId, activeTab) change

Each composable cleans up its own subscription via onScopeDispose. Switching eventId tears down the previous subscriptions before registering new ones. Switching the active market-category tab swaps only the markets-by-group subscription — the match + market-groups subscriptions stay attached.

Default tab auto-selection runs once per eventId when the market groups arrive: the group with isDefault: true wins, falling back to the first group. If the user (or a prior interaction) has moved the tab off 'allMarkets', the auto-select respects the current choice.

Trigger contract — goma:event-navigate

When followEventNavigate: true (default), the widget listens on the window for goma:event-navigate events with payload { eventId, homeTeam, awayTeam, competitionName } and patches its own eventId. All four <goma-events-horizontal> card components and the horizontal wrapper emit this event today on card tap.

To opt out:

js
el.config = { ...el.config, followEventNavigate: false }

Hosts that want to mediate (e.g., route through their own URL before switching) can also event.stopPropagation() on goma:event-navigate at a higher DOM level.

Props (HTML attributes + JS properties)

HTML attributeJS propertyTypeDefaultNotes
event-ideventIdstring | nullnullPrimary contract. Setting it re-mounts the subscriptions.
localelocalestring'en'Mirrored.
configGameDetailsConfig{}JS-only; full config object.
themeRecord<string, string>{}Standard --goma-* token bag.
follow-event-navigatefollowEventNavigatebooleantrueToggle the navigate watcher.
initial-batch-sizeinitialBatchSizenumber5Markets in the first batch.
batch-sizebatchSizenumber10Markets added per sentinel hit.
intersection-root-marginintersectionRootMarginstring'200px'Passed to IntersectionObserver.
show-back-buttonshowBackButtonbooleantrueToggle the header back affordance.
show-competitionshowCompetitionbooleantrueToggle the competition row.
show-live-clockshowLiveClockbooleantrueToggle the live clock (effective only for live events).

Events

All events use { bubbles: true, composed: true }.

EventDetailWhen
goma:back{ eventId: string }User taps the back affordance in the header. Host decides what "back" means in its routing.
goma:tab-select{ eventId, groupKey, groupName }User changes the active market-group tab — or the auto-select default fires.
goma:event-not-found{ eventId: string }Match subscription resolves and the event is missing or empty.
goma:outcome-select{ bettingOfferId, outcomeName, odds, eventId, marketId }User taps an outcome to add to the betslip (reused from the shared RegularOdds button).
goma:outcome-deselect{ bettingOfferId }User taps an active outcome to remove.
goma:retry{}User taps the retry button on the error boundary.

Empty / loading / error states

  • Inert — no eventId set: nothing renders.
  • LoadingeventId set, subscription in flight: skeleton (header outline + 3 placeholder market cards).
  • Not found — subscription resolved, no event in the store: empty state with empty_state_match_not_available_* strings, plus a goma:event-not-found emit.
  • No markets — match loaded but the active tab has no markets: empty state with empty_state_match_no_markets_* strings.
  • Render error — caught by the in-shadow error boundary: red banner with a retry button that emits goma:retry.

Embedding examples

Vanilla HTML

html
<div style="height: 100vh; display: grid; grid-template-rows: auto 1fr;">
  <goma-events-horizontal id="evts"></goma-events-horizontal>
  <goma-game-details id="gd"></goma-game-details>
</div>
<script type="module">
  import '@gomagaming/events-horizontal'
  import '@gomagaming/game-details'

  const cfg = {
    apiBaseUrl: 'wss://sports-api.example.com',
    locale: 'en',
  }
  document.getElementById('evts').config = { ...cfg, topic: 'live/1' }
  document.getElementById('gd').config = cfg
  // No glue needed — tapping a card emits goma:event-navigate
  // and game-details patches its eventId via the watcher.
</script>

React

jsx
import '@gomagaming/game-details'

function MatchPage({ eventId, apiBaseUrl }) {
  return (
    <goma-game-details
      ref={(el) => {
        if (!el) return
        el.config = { apiBaseUrl, locale: 'en' }
        el.eventId = eventId
      }}
    />
  )
}

Vue

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

const props = defineProps({ eventId: String })
const el = ref(null)
const config = { apiBaseUrl: 'wss://sports-api.example.com', locale: 'en' }

onMounted(() => {
  el.value.config = config
  el.value.eventId = props.eventId
})

watch(() => props.eventId, (id) => { if (el.value) el.value.eventId = id })
</script>

<template>
  <goma-game-details ref="el" @goma:back="$emit('back')" />
</template>

Theming

Reuses the existing --goma-* token set; five new tokens are specific to this widget and default to existing tokens if you don't override them. See Theming → Game details for the full table:

  • --goma-tabPillBackground / --goma-tabPillBackgroundActive / --goma-tabPillText / --goma-tabPillTextActive — market-tab pills
  • --goma-marketCardHeaderBackground — market card title strip
js
el.theme = {
  tabPillBackgroundActive: '#7c4dff',
  tabPillTextActive: '#ffffff',
  marketCardHeaderBackground: '#1a1d3a',
}

i18n / Message overrides

The widget bundles its strings via @gomagaming/core's shared messages. Pass config.messages to override per-locale phrases:

js
el.config = {
  apiBaseUrl: '…',
  messages: {
    en: {
      empty_state_match_not_available_title: 'Match unavailable',
      empty_state_match_no_markets_title: 'No markets yet',
      market_categories: 'Categories',
    },
  },
}

Keys used by the widget:

  • back, live, expand, collapse, retry
  • market_categories (the ARIA label of the tablist)
  • market_group_<key> (per-category labels — fall back to the server-provided translatedName when present)
  • empty_state_match_not_available_title / empty_state_match_not_available_description
  • empty_state_match_no_markets_title / empty_state_match_no_markets_description

Accessibility

  • The market-tab strip is an ARIA tablist; each pill is a tab with aria-selected; the markets list is the corresponding tabpanel with aria-labelledby pointing to the active tab.
  • The back affordance is a <button> (not an anchor) — the widget doesn't know the host's route.
  • Each market card's collapse toggle exposes aria-expanded and aria-controls.
  • Outcome buttons (shared RegularOdds component) carry aria-pressed for the selected state and aria-disabled when suspended.

Source layout

LayerFile
Element classpackages/game-details/src/GameDetailsElement.js
App wrapperpackages/game-details/src/GameDetailsApp.vue
Per-instance statepackages/game-details/src/composables/useGameDetails.js
Headerpackages/game-details/src/components/GameDetailsHeader/GameDetailsHeader.vue
Header subscriptionpackages/game-details/src/components/GameDetailsHeader/GameDetailsHeader.subscription.js
Market tabspackages/game-details/src/components/GameDetailsMarketTabs/GameDetailsMarketTabs.vue
Markets listpackages/game-details/src/components/GameDetailsMarketsList/GameDetailsMarketsList.vue
Markets list subscriptionpackages/game-details/src/components/GameDetailsMarketsList/GameDetailsMarketsList.subscription.js
Market cardpackages/game-details/src/components/GameDetailsMarketsList/GameDetailsMarketCard.vue
Empty statepackages/game-details/src/components/EmptyState/EmptyState.vue
Shared score machinerypackages/sports-domain/src/composables/games/gamecards/{useGameCardsTallOdds,useGameScoresTallOdds,useGamePartsAndTimesTallOdds}.js
WAMP actionspackages/sports-domain/src/api/everymatrix/actions.js (createMatchDetailsActions)

What's deferred

These are recognised next-version surfaces, not omissions:

  • BetBoost tab — a bet_boost synthetic tab that wraps the existing BetBoostList widget concern. Source ships it but the gomawt port lands separately.
  • Share button + goma:event-share event contract — the source sportsbook ships a header share affordance; gomawt has no share contract yet.
  • Favorite toggle in the header.
  • Stats / lineups / H2H / commentary — these aren't implemented in the source MatchDetails page either (dead-code references in useMatchDetails only).
  • <goma-game-details-dialog> twin — a modal variant for mobile-overlay UX patterns. Will land if a real product need shows up.