Skip to content

Introduction

What is gomawt?

gomawt is a library of framework-agnostic Web Components that render a sportsbook UI — event cards, market cards, sport bars, competition lists, and more. Each widget ships as its own NPM package under the @gomagaming/* scope. Consumers install only the widgets they need and embed them with a single ESM import plus a plain JavaScript config object — no Vue knowledge required, no build-step changes, no framework lock-in.

The repo is structured as a pnpm workspaces monorepo with one package per widget.

html
<goma-events-horizontal id="ev"></goma-events-horizontal>

<script type="module">
  import '@gomagaming/events-horizontal'

  document.querySelector('#ev').config = {
    apiBaseUrl: 'https://api.example.com',
    locale: 'en',
    socketUrl: 'wss://socket.example.com',
    socketRealm: 'example',
    ucsOperatorId: '4313',
  }
</script>

The same widget drops into React, Vue, Angular, or Svelte with no adapter library — because native custom elements are part of the browser, not the framework.

Who is this for?

  • Host-app developers who want to embed a full-featured sportsbook in minutes without rebuilding one from scratch.
  • Operators who need a single UI library that works across marketing sites, apps, and partner white-labels.
  • Contributors who want to extend the library with new widgets or re-skin existing ones without touching consumer code.

How it works

Every gomawt widget is a real custom element that, under the hood, runs an isolated Vue 3 application inside a Shadow DOM.

The anatomy of one instance

<goma-events-horizontal>                   HTMLElement subclass
  #shadow-root (open)
    adoptedStyleSheets                     Tailwind + DaisyUI + theme CSS
    <div>                                  mount point
      Vue app (createApp)
        Pinia (createPinia)                isolated store tree
        VueRouter (createMemoryHistory)    no URL side effects
        VueI18n                            locale from config/attribute
        App.vue                            widget wrapper
          Inner component tree             rendered into the shadow root

Five things happen when the element is connected to the DOM:

  1. attachShadow({ mode: 'open' }) creates a private DOM subtree.
  2. createApp() spins up a fresh Vue app inside that shadow root.
  3. Isolated Pinia, Vue Router (memory history), and vue-i18n are installed on that app.
  4. All CSS (Tailwind, DaisyUI, theme tokens) is injected via adoptedStyleSheets, falling back to a <style> tag in older browsers.
  5. A reactive() object bridges the HTMLElement property API to the Vue tree.

The reactive bridge

The element class holds one reactive state object that Vue watches. Element property setters mutate it, and Vue re-renders automatically:

js
// Inside the element class
this._state = reactive({ events: [], locale: 'en', theme: {} })

// The app is given access
app.provide('gomaState', this._state)

// JS property setters mutate the reactive state
set events(val) { this._state.events = val }

Components inside the widget read that state via inject('gomaState'), and host code drives the widget simply by setting properties:

js
el.events = [{ id: '123', /* ... */ }]

Talking back to the host

Widgets dispatch CustomEvents with composed: true so the events cross the Shadow DOM boundary and bubble up to the host app:

js
this.dispatchEvent(new CustomEvent('goma:outcome-select', {
  bubbles: true,
  composed: true,
  detail: { bettingOfferId, outcomeName, odds }
}))

The host listens with standard DOM APIs:

js
el.addEventListener('goma:outcome-select', (e) => {
  addToBetslip(e.detail)
})

Standalone vs hosted mode

Every widget supports two runtime modes, auto-selected at mount time:

ModeTriggerWhat happens
StandaloneElement has no parent goma orchestrator.Own Shadow DOM, own Vue app, own Pinia, own router, own API container. Suitable for drop-in usage on any page.
HostedElement is a descendant of another goma element that already has a Vue app.No new Shadow DOM, no new Vue app. Renders as a VNode inside the parent's Vue context, sharing Pinia/i18n/router. The parent's shadow root provides CSS isolation.

Hosted mode is forward-looking: it lets a future orchestrator widget compose multiple child widgets — sharing one Pinia, one i18n, one router — without each child standing up its own Shadow DOM. Today only <goma-events-horizontal> ships, so it always runs in standalone mode.

Runtime configuration

All configuration is runtime, not build-time. There is no import.meta.env, no per-consumer build. The config property replaces what env vars would normally carry:

js
el.config = {
  apiBaseUrl: 'https://...',
  socketUrl: 'wss://...',
  socketRealm: '...',
  ucsOperatorId: '...',
  locale: 'en',
  theme: { /* CSS variable overrides */ },
}

The config is validated on assignment (unknown keys log a warning), applied to the reactive state, and provided into the Vue tree via inject('sportsbookConfig').

Key architectural decisions

DecisionWhy
createApp() + Shadow DOM, not defineCustomElement()Our widgets are full SPAs with routers and store trees. Vue's defineCustomElement() is designed for isolated leaf components and can't host a router or sub-widgets cleanly.
createMemoryHistory() for the routerWidgets must never touch the host page's URL. Memory history keeps navigation internal.
Vue, Pinia, vue-router, vue-i18n, @vueuse/core declared as peerDependenciesOne copy across all installed widgets — bundlers dedup. Consumers install the framework deps once at the app level.
CSS via adoptedStyleSheetsKeeps styles inside the Shadow DOM. Never leaks into document.head.
reactive() bridge between element and VueClean separation. Element properties drive reactive state; Vue watches trigger re-renders.
Custom events with composed: trueEvents cross the shadow boundary to reach host listeners.
Separate createPinia() per instanceMultiple widgets on the same page don't collide. The host app's Pinia is unaffected.
Runtime config replaces env varsConsumers configure the widget at runtime — there is no per-consumer build step.
  • Theming — Canonical CSS-variable token reference for every widget.
  • Widget reference — API docs for every widget shipped by gomawt.