<goma-search>
Sports search. A search field that shows matching events, competitions and teams while the player types, and takes them straight to the one they pick. It renders either as a field with a results panel hanging below it (for a site header or a sidebar) or, at phone width, as a full-screen surface that opens when the field is tapped. Picking a result announces it the way the rest of GomaPulse announces the same thing, so a game-details widget or an events feed already on the page follows the pick without any wiring.
Element tag
html
<goma-search></goma-search>Installation
bash
pnpm add @gomagaming/search vue pinia vue-router vue-i18n @vueuse/coreThe five framework packages are peer dependencies shared across every @gomagaming/* widget: install them once for the whole set. See Installation for the one-time .npmrc token setup.
Configuration
js
document.querySelector('goma-search').config = {
socketUrl: 'wss://…', // required for dataSource 'rpc': search is a WAMP call
locale: 'en', // 'en' | 'fr' | 'pt'
theme: { … }, // --goma-* overrides
messages: { … }, // per-locale phrase overrides
debug: false,
}| Key | Type | Default | Notes |
|---|---|---|---|
socketUrl | string | - | The shared WAMP connection. Without it the widget has nothing to ask and shows its error state rather than a spinner. |
locale | string | 'en' | Also settable as the locale attribute. |
theme | object | {} | See Theming. |
messages | object | {} | See i18n. |
network.rpcTimeoutMs | number | 15000 | Connection-wide. The search path caps its own wait at 8s regardless: a player who typed six seconds ago has retyped. |
Data sources
dataSource picks where results come from.
'rpc' (default): the provider's multiSearch call over the shared socket. One request per typing pause, never one per keystroke, and a response that a newer request has already overtaken is discarded rather than rendered.
html
<goma-search data-source="rpc"></goma-search>'mock': a bundled fixture filtered in-process, with a simulated 250ms round trip. It is what the playground runs on, and it is useful for building a layout with no connection. The fixture is loaded lazily, so it is not in the bundle a host ships.
html
<goma-search data-source="mock"></goma-search>Events and competitions are asked for by default. A competition name matches no event record, so the Competitions group is what answers a search for "premier league"; set searchCompetitions to false to search events only. Teams are asked for only when searchTeams is on, and it defaults to off (see What's deferred). A group with no hits simply does not appear.
A competition with no upcoming match and no live match is left out of the Competitions group, because opening it would show an empty competition. A competition whose only event is an outright is left out for the same reason.
Props / attributes
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
query | query | string | '' | What is in the field. Two-way: typing writes it, and setting it drives the widget. |
minChars | min-chars | number | 3 | Characters required before anything is asked of the provider. |
limit | limit | number | 10 | Maximum records per group. |
debounceMs | debounce-ms | number | 350 | Pause after the last keystroke before the request leaves. |
mode | mode | string | 'auto' | 'inline', 'overlay', or 'auto': overlay below mobileBreakpoint, inline above it. |
mobileBreakpoint | mobile-breakpoint | string | number | 'sm' | Tailwind token (sm…2xl) or a pixel value. |
open | open | boolean | false | Whether the search surface is open. Writable, so a host can open it from its own button. |
showRecents | show-recents | boolean | true | Offer recent searches when the field is focused while empty. |
recentsLimit | recents-limit | number | 6 | How many are kept, most recent first. |
recentsStorageKey | recents-storage-key | string | 'goma:search:recents:v1' | Shared by default, so two search widgets offer the same history. Give each its own key to separate them. |
searchCompetitions | search-competitions | boolean | true | Also ask for competitions. search-competitions="false" searches events only. |
searchTeams | search-teams | boolean | false | Also ask for teams. |
teamSelectBehaviour | team-select-behaviour | string | 'search' | 'search' re-runs the search for the team's name; 'announce' leaves the next step to the host. |
dataSource | data-source | string | 'rpc' | 'rpc' or 'mock'. |
searchStatus | - | string | 'idle' | Read it, don't set it: 'idle', 'loading', 'results', 'empty' or 'error'. |
searchResults | - | object | { events: [], competitions: [], teams: [] } | Read it, don't set it: the records currently rendered. |
searchTotal | - | number | 0 | Read it, don't set it: totalRecordsMatched from the provider. |
searchSettledQuery | - | string | '' | Read it, don't set it: the query the rendered results belong to, which is usually a keystroke or two behind query. |
Boolean attributes follow the house rule: anything except the string false is true, so show-recents="false" is how you turn one off from markup.
Events
| Event | detail | When |
|---|---|---|
ready | {} | Mounted and first render complete. |
goma:error | { message, code, component? } | An error the in-shadow boundary could not handle. |
goma:retry | {} | The error boundary's Retry was tapped. |
goma:search-open | { mode } | The surface opened. mode is 'inline' or 'overlay'. |
goma:search-close | { reason } | The surface closed. reason is 'select', 'escape', 'outside', 'backdrop', 'back' or 'native'. |
goma:search-select | { kind, id, name?, query } | Any result was picked. Fires alongside the kind-specific event below. |
goma:event-navigate | { eventId, homeTeam, awayTeam, competitionName } | An event was picked. |
goma:competition-select | { competitionId, competitionName, competitionSportId, previousCompetitionId } | A competition was picked. previousCompetitionId is always null: search has no prior selection of its own. |
Integrates with
This widget only emits; it follows nothing and has no follow* property of its own.
Picking an event emits goma:event-navigate (the same event an event card emits) so a <goma-game-details> on the page opens it with no host code (its followEventNavigate defaults to true). Picking a competition emits goma:competition-select, the payload <goma-top-competitions-pills> and <goma-competitions-list> emit, so a <goma-events-horizontal> with followCompetitionPicker (default true) re-scopes its feed to it. See Cross-widget wiring for the full matrix.
One caveat worth knowing before you pair it with a feed: a feed following the competition picker rewrites only the competition segment of its topic. Pick a competition from a different sport than the feed is showing and the feed is left with a topic that mixes the two. Decide in the host whether to also set the feed's sportId.
It writes no shared-state envelope. Recent searches live under their own localStorage key (recentsStorageKey), which is not one of the shared-state envelopes and is never read by another widget.
Empty / loading / error states
| State | What renders | Reached when |
|---|---|---|
| Idle | Recent searches, or nothing at all when there are none | Fewer than minChars characters. Not an empty result: nothing has been searched. |
| Loading | A spinner, with aria-busy | From the first keystroke past minChars until the request settles. Always resolves: the 8s cap turns a hung call into the error state. |
| Results | The groups that have hits, in the order Events, Competitions, Teams | At least one record came back. A group with no hits is left out entirely rather than shown empty. |
| Empty | "No Results for what was typed in sports" | The provider matched nothing. Names searchSettledQuery, not what is in the field now. |
| Error | A short message and a Retry button | The call failed, timed out, came back malformed, or there is no connection to make it on. Retry re-runs the current query immediately, skipping the debounce. |
An error never empties the rest of the page, and a search never replaces the list of a co-mounted events feed.
Theming
Reads existing --goma-* tokens only: no token was introduced for this widget.
| Token | Paints |
|---|---|
--goma-inputBackgroundSecondary | The search field |
--goma-backgroundPrimary | The inline results panel |
--goma-backgroundCards | The highlighted row |
--goma-dialogBackground | The full-screen overlay |
--goma-textPrimary | Result text |
--goma-textSecondary | Group labels, competition and kick-off lines, placeholder |
--goma-iconPrimary | Magnifier, clear and back controls |
--goma-highlightPrimary | Focus ring, Retry button |
--goma-separatorLineSecondary | Row separators |
--goma-alertError | The error-boundary banner |
See Theming for the full reference.
i18n / message overrides
All text comes from the bundled locales and follows locale. The keys this widget uses, with their en defaults:
| Key | en |
|---|---|
search_in_sportsbook | Search in sportsbook |
search_for_teams_or_competitions | Search for Teams or Competitions... |
search_results | Search Results |
search_results_found | {count} results found |
no_results_search | No Results for {value} in sports |
search_error | Search is unavailable right now. |
events | Events |
competitions | Competitions |
teams | Teams |
recent_searches | Recent searches |
clear | Clear |
clear_all | Clear all |
retry | Retry |
loading | Loading |
back | Back |
Override any of them per locale through config.messages.
Accessibility
The field is a combobox: role="combobox" with aria-expanded, aria-autocomplete="list", aria-controls pointing at the results listbox, and aria-activedescendant naming the highlighted row. Focus never leaves the input (the highlight moves by id) which is what keeps the on-screen keyboard up on a phone.
- Down / Up move through every result in render order, crossing group boundaries and wrapping at both ends. Enter picks the highlighted row; Escape closes.
- The whole flow (focus, type, choose) works from the keyboard alone.
- The result count is announced through a polite live region whenever the list settles, and the highlighted row carries
aria-selected="true". - Group headings are
role="presentation": one listbox spans all three groups, so the arrow keys are not trapped inside one of them. - The overlay is a native
<dialog>opened withshowModal(), so the browser's own focus trap applies, the page behind it is scroll-locked, and the panel isinertwhile closed. Closing returns focus to the field it was opened from. - The field is pinned to 16px so focusing it does not zoom the page on iOS Safari.
What's deferred
- Teams are off by default. The rendering, the request and the events are all in place, but the provider's team (
PARTICIPANT) search comes back carrying competitions and matches rather than teams. TurningsearchTeamson today lists those under the Teams heading. - Accent-insensitive matching is the provider's. The widget folds accents for its own comparisons (de-duplicating recent searches, and filtering the mock fixture) but whether a live search for "besiktas" finds "Beşiktaş" is decided server-side.
- No pagination. The provider returns
totalRecordsMatchedand the widget surfaces it assearchTotal, but there is no "load more": the list is capped atlimitper group. - No casino results. This widget searches sports only.
Changelog
Current release: 0.37.11. Every GomaPulse package shares one version number and they are published together, so a release that changes nothing here still bumps this widget. Only the releases that changed it are listed below.
Every release that changed this widget, newest first. The latest is open; the rest expand.
0.37.9
- Search: the Competitions group no longer lists a competition that has no upcoming match and no live match. Opening such a result showed an empty competition. A competition whose only event is an outright is also left out, because it has no match to list.
searchEvents()in sports-domain applies the filter, so any caller of it gets the same result.
0.37.2
<goma-search>finds competitions by default.searchCompetitionsnow defaults totrue. It was off because the provider had not confirmed that its search answers competition queries; it does, with the fields the Competitions rows already read. With the group off, typing a competition name returned "No Results": a competition name matches no event record, so the only search that was being sent could not find it, under a placeholder that reads "Search for Teams or Competitions...".A host that wants the previous events-only behaviour sets
search-competitions="false"(orel.searchCompetitions = false).searchTeamsstays off. The provider's team search comes back carrying competitions and matches rather than teams, so the group would list the wrong records under a Teams heading.sports-domainchanges no behaviour: the comment onsearchEventssaid the widget keeps both groups off, and now says what the provider returns for each search map.
0.35.0
New widget:
<goma-search>. Sports search with grouped live results.A search field that shows matching events (and, behind a flag, competitions and teams) while the player types, in one of two presentations on the same element: an inline panel under the field for a header or a sidebar, and a full-screen overlay at phone width, resolved automatically by
mode: 'auto'againstmobileBreakpoint. The overlay is a native<dialog>opened withshowModal()(the browser's own focus trap, a scroll-locked page behind it,inertwhile closed, and focus returned to the field on close) the same recipe<goma-sports-navigation-dialog>uses. The field is a combobox: arrow keys walk one flat list across all three groups,aria-activedescendantmoves the highlight without moving focus (so a phone keyboard stays up), Enter picks, Escape closes, and the result count goes out through a polite live region. Recent searches are kept inlocalStorage, de-duplicated accent- and case-insensitively, capped, and clearable.Picking a result announces it the way the rest of the family announces the same thing (
goma:event-navigatefor an event,goma:competition-selectfor a competition) so a co-mounted<goma-game-details>or an events feed follows the pick with no host wiring. Picking a team re-runs the search for that team's name by default, or announces it instead viateamSelectBehaviour: 'announce'; it is never a dead click either way.sports-domaingainsuseSearchSource, which owns what a one-shot RPC helper cannot: a debounce, and a race guard that drops any response a newer request has overtaken. Type "man", wait, finish "manchester", and the list settles on "manchester" even when the first response arrives second. It caps a search at 8s regardless of the connection-widerpcTimeoutMs, and reports "no socket configured" as the widget's error state rather than a spinner that never resolves. The emptysearchaction namespace is now implemented assearchSports(), andsearchEvents()takesincludeCompetitions/includeTeams, which add TOURNAMENT and PARTICIPANT search maps and map whatever comes back.Both group flags default to off: the provider has not confirmed it answers those two searches, and the sportsbook this code came from carried a TOURNAMENT map commented out with no note explaining why. Turning them on asks for the groups and renders what arrives, which may be nothing.
coregains the keys this widget needs in all three locales (events,teams,search_results_found,search_error) plus the French and Portuguese translations ofno_results_searchandsearch_in_sportsbook, which existed only in English.Match dates read day-first in English, so one match is no longer written two different ways across the suite.
An event card writes the kick-off as a compact
11/10;<goma-game-details>'s header,<goma-hero-card>,<goma-multi-banners>and<goma-search>'s result rows write it in words throughtoLocaleDateString. Withlocale: 'en'(the bare tag, whichIntlresolves to American ordering) that long form came out asOctober 11next to the card's11/10. English was the only shipped locale that disagreed with its own cards (frandptare already day-first, and the shared time formatter has always been pinned toen-GB), so the long form now resolves a bareentoen-GB. A host that explicitly configuresen-USkeeps American ordering.Two surfaces build their own
Intl.DateTimeFormatrather than going through the shared formatters, and both showed the same inversion for the same match: the My Bets card's leg date (which also printed07 Octoberwhere the cards print7 October, and sat directly under a hand-builtdd/mm/yyyy"placed" line) and the live-chat bet modal's per-leg date. Both now resolve the locale the same way.Every package listed here renders a date that changes, which is why the note reaches all of their pages rather than only the two whose source moved: including the two betslips and
<goma-messenger>, which render the same leg date through the<goma-my-bets>widget they embed.Not covered, and deliberately: the missions surfaces format challenge dates with the browser's locale rather than the widget's, and, in the same file, render the same instant as a deliberately locale-independent
DD/MM/YYtwo functions below, so one view contradicts itself regardless of locale. That is a different defect from this one and wants its own ticket.Review fixes: a throwing property rescue, a split event sentinel, a duplicated rule
The pre-upgrade rescue could stop a widget mounting.
deleteon a non-configurable own property throws, and module code is always strict, so a host that usedObject.defineProperty(el, 'config', { value })before the widget loaded (legal, andconfigurabledefaults tofalse) madeconnectedCallbackthrow. The element never mounted at all: an uncaughtTypeErrorand no widget, which is worse than the silent drop the rescue exists to prevent, and it lands in the same window. Such a property can never be rescued (the own definition shadows the accessor for the element's life), so it is now skipped with a warning naming the key, and the rest of the element's properties still apply.goma:competition-selectcarried two "unknown" sentinels.top-competitions-pillsdescribed an unresolved name as''whilecompetitions-listandtop-competitionsusednull. Three widgets emit that one event and a host wires a single listener for it, so the difference landed on every consumer.nullthroughout now.isFeaturedwas derived in two places. The predicate lived in bothuseCompetitionSelectionandTopCompetitionsList.vue. They agreed, but two copies agreeing is not the same as agreeing: if one moved, the payload would claim a rendering the user never saw. The exact failure deriving it was meant to prevent. The composable now exports the predicate and the template renders from it, so agreement is structural.<goma-search>landed on main while this was open and emits the same event with the old'', so it is aligned here too: otherwise the split it fixes would return the moment this merges.