Building blocksInstallFramework 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: `
    <button (click)="open()">Book appointment</button>
    <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 = "";
  }

  open() {
    this.medos?.open();
  }

  /** 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);
    });
  }
}

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.