What you will build

A Shared counter widget. Every participant sees the same number and a +1 button; a participant who joins later receives the current value; others get a notification when someone clicks.

A site widget has two parts: a connector, which exchanges messages with other participants, and a widget, which shows the UI. Both are plain classes in your page's code.

1. Wait for Weei

Site widgets use the RxJS instance that Weei provides, so their observables work with Weei's.

JavaScript
const weei = await window.weeiReady;
const { rxjs } = weei.sdk;

2. The connector

createEvent(name) returns a channel: next(value) sends to every participant, next(value, peerId) to one, and subscribing receives messages from others. lastMessageFrom is the sender of the message being handled.

JavaScript
const { rxjs } = weei.sdk;

class CounterConnector {
  // The name peers use to address this connector. Keep it stable and unique.
  static connectorName = 'acme-counter';

  value$ = new rxjs.BehaviorSubject(0);
  remoteIncrement$ = new rxjs.Subject();

  initListeners(gateway) {
    this.count = gateway.createEvent('count');

    // Messages from peers are untrusted: check them before use.
    this.count.subscribe(value => {
      if (!Number.isSafeInteger(value) || value <= this.value$.value) return;
      if (value === this.value$.value + 1) {
        this.remoteIncrement$.next({ from: this.count.lastMessageFrom, value });
      }
      this.value$.next(value);
    });

    // A participant who joins later receives the current value.
    gateway.reachablePeers$.subscribe(peers =>
      peers.forEach(peer => this.count.next(this.value$.value, peer))
    );
  }

  increment() {
    this.value$.next(this.value$.value + 1);
    this.count.next(this.value$.value);
  }
}

The highest value wins, so two simultaneous clicks may count as one. That keeps the example short; a real widget would merge changes per participant.

3. The widget

mount(app) builds the UI and returns its root element; Weei places it on the page, here in a movable window because of windowed, and removes it when the widget closes. styles is added while the widget is open. backgroundConnectors start as soon as the widget is added, so the counter keeps its value while the window is closed.

JavaScript
class CounterWidget {
  widgetId = 'acme-counter';
  widgetTitle = 'Shared counter';
  widgetIcon = 'exposure_plus_1';
  optional = true;
  windowed = true;
  styles = `
    .acme-counter { width: 200px; padding: 16px; display: grid; gap: 12px; justify-items: center; }
    .acme-counter output { font-size: 40px; font-weight: 700; }
  `;
  backgroundConnectors = [CounterConnector];
  destroy$ = new rxjs.Subject();

  mount(app) {
    const root = document.createElement('section');
    root.className = 'acme-counter';
    const value = document.createElement('output');
    const button = document.createElement('button');
    button.textContent = '+1';
    root.append(value, button);

    app.rtcService.getOrCreateConnector(CounterConnector)
      .pipe(rxjs.first(), rxjs.takeUntil(this.destroy$))
      .subscribe(counter => {
        button.onclick = () => counter.increment();
        counter.value$
          .pipe(rxjs.takeUntil(this.destroy$))
          .subscribe(count => (value.textContent = String(count)));
      });

    // Weei places the returned root on the page, here in a movable window.
    return root;
  }

  destroy() {
    this.destroy$.next();
  }
}

4. Notifications

configureBackground(app, allowed) runs while the widget is available, even with its window closed. Use it with app.snackbar to tell users what others did. The display name comes from a core connector in weei.sdk.connectors. Notifications with the same key update one snackbar instead of stacking.

JavaScript
class CounterWidget {
  // …members from the previous step…

  configureBackground(app, allowed) {
    this.notifications?.unsubscribe();
    this.notifications = undefined;
    if (!allowed || !app.snackbar) return;

    const { connectors } = weei.sdk;
    this.notifications = rxjs.combineLatest([
      app.rtcService.getOrCreateConnector(connectors.TreeRtcServiceEventsConnector),
      app.rtcService.getOrCreateConnector(CounterConnector)
    ]).pipe(
      rxjs.first(),
      rxjs.switchMap(([names, counter]) => counter.remoteIncrement$.pipe(
        rxjs.map(event => ({ ...event, name: names.getDisplayName(event.from) || 'Someone' }))
      ))
    ).subscribe(({ name, value }) => app.snackbar.show({
      message: name + ' increased the counter to ' + value,
      key: 'acme-counter',
      actionLabel: 'Open',
      onAction: () => app.widgetsApi.enable(this.widgetId)
    }));
  }
}

5. Add the widget

JavaScript
const handle = await weei.addWidget(CounterWidget);

// Open it from your own UI:
await weei.widgetsApi.enable('acme-counter');

// Remove it when it is no longer needed:
handle.remove();

The widget also gets a tile in the control panel. Open the page in two browser tabs and press +1 in one of them: both show the new value, and the other tab shows a notification.

Next

See the site widget reference for every member and rule.