Get startedInstallFramework guides

Angular

Standalone Angular component that loads the CDN bundle.

src/app/booking.component.ts
import { Component, OnDestroy, OnInit } from "@angular/core";

const CDN_URL = "https://widgets.medos.one/v2/unified.js";
const CONTAINER_ID = "medos-booking-inline";

type MedosBookingApi = {
  init: (config: Record<string, unknown>) => void;
  open: (config?: Record<string, unknown>) => void;
  close: () => void;
};

@Component({
  selector: "app-booking",
  standalone: true,
  template: `
    <div id="${CONTAINER_ID}" style="min-height: 600px"></div>
  `,
})
export class BookingComponent implements OnInit, OnDestroy {
  private destroyed = false;

  /** Read through `window` so a missing global is undefined, not a crash. */
  private get medos(): MedosBookingApi | undefined {
    return (window as unknown as { MedosBooking?: MedosBookingApi })
      .MedosBooking;
  }

  ngOnInit() {
    this.load()
      .then(() => {
        // The user may have navigated away while the bundle was downloading;
        // initializing now would target a container that no longer exists.
        if (this.destroyed) return;
        this.medos?.init({
          apiKey: "mk_your_publishable_key",
          mode: "inline",
          containerId: CONTAINER_ID,
        });
      })
      .catch((err) => console.error(err));
  }

  ngOnDestroy() {
    this.destroyed = true;
    // Inline mode has no close(); clear the container so navigating back
    // doesn't mount a second widget on top of the first.
    const el = document.getElementById(CONTAINER_ID);
    if (el) el.innerHTML = "";
  }

  /** Reuses an existing tag so navigation doesn't re-download the bundle. */
  private load(): Promise<void> {
    if (this.medos) return Promise.resolve();

    const existing = document.querySelector<HTMLScriptElement>(
      `script[src="${CDN_URL}"]`,
    );
    if (existing) {
      return new Promise((resolve) =>
        existing.addEventListener("load", () => resolve(), { once: true }),
      );
    }

    return new Promise((resolve, reject) => {
      const script = document.createElement("script");
      script.src = CDN_URL;
      script.async = true;
      script.onload = () => resolve();
      script.onerror = () => reject(new Error("Medos Booking failed to load"));
      document.head.appendChild(script);
    });
  }
}

init({ mode: "modal" }) opens the modal as soon as it runs, so don't call it on mount. Load the script the same way, then start the widget from the button — init() on the first click, open() after that:

  private started = false;

  book() {
    const m = this.medos;
    if (!m) return; // bundle still loading
    if (this.started) return m.open();
    this.started = true;
    m.init({ apiKey: "mk_your_publishable_key", mode: "modal" });
  }
<button (click)="book()">Book appointment</button>

Don't reach for the bare global

declare const MedosBooking makes TypeScript happy but doesn't help at runtime — if the bundle hasn't loaded, evaluating the identifier throws ReferenceError, and MedosBooking?.open() will not protect you. Optional chaining only guards null/undefined on a binding that exists. Go through window as above.

On this page