DEV Community

Cover image for Streamlining Scheduling: Integrating Cal.com with SvelteKit & Svelte 5
Michael Amachree
Michael Amachree Subscriber

Posted on

Streamlining Scheduling: Integrating Cal.com with SvelteKit & Svelte 5

Are you looking to seamlessly integrate powerful scheduling capabilities into your SvelteKit application without the hassle of complex external scripts? Cal.com provides a robust solution, and with Svelte 5's innovative runes, embedding its features is not just straightforward, but elegantly Svelte-native. This blog post will guide you through integrating Cal.com's inline, floating popup, and element-click embeds into a SvelteKit application, leveraging Svelte 5's new reactivity system, known as runes, for a cleaner, more performant, and truly Svelte-native implementation.

Our Modular Approach

To keep our SvelteKit project organized and maintainable, we'll adopt a modular approach. This involves creating a dedicated utility module for managing the Cal.com script and two Svelte components to handle the different embed types:

  • cal.js: A utility module responsible for loading the Cal.com embed script and providing functions to initialize namespaces and call Cal.com's API methods.
  • Cal.svelte: A versatile Svelte component that handles both inline and floatingButton embed types. It also takes care of initializing namespaces for elementClick embeds.

This solution ensures modularity, reusability, and alignment with Svelte 5’s runes-based reactivity.

Core Components Explained

Let's briefly look at the role of each piece of the solution:

1. cal.js - The Script Manager

This JavaScript module acts as the central point for managing the Cal.com embed script. Its primary functions are:

  • initCal(): Ensures the Cal.com embed script (https://app.cal.com/embed/embed.js) is loaded into the document's <head> only once, preventing duplicate script injections.
  • initNamespace(namespace, options): Initializes a specific Cal.com namespace (e.g., '15min') with given options, such as the origin URL. This function ensures that a namespace is only initialized if it hasn't been already.
  • callNamespaceMethod(namespace, method, ...args): Provides a safe way to call methods (like "inline", "floatingButton", or "ui") on a specific Cal.com namespace, with built-in checks to ensure the namespace is initialized.
// @ts-nocheck Can't determine types for Cal

const initializedNamespaces = new Set();

export function initCal() {
    // if (window.Cal) return; // Keep this commented or remove if Cal can be re-initialized safely
    (function (C, A, L) {
        let p = function (a, ar) {
            a.q.push(ar);
        };
        let d = C.document;
        C.Cal =
            C.Cal ||
            function () {
                let cal = C.Cal;
                let ar = arguments;
                if (!cal.loaded) {
                    cal.ns = {};
                    cal.q = cal.q || [];
                    d.head.appendChild(d.createElement('script')).src = A;
                    cal.loaded = true;
                }
                if (ar[0] === L) {
                    const api = function () {
                        p(api, arguments);
                    };
                    const namespace = ar[1];
                    api.q = api.q || [];
                    if (typeof namespace === 'string') {
                        cal.ns[namespace] = cal.ns[namespace] || api;
                        p(cal.ns[namespace], ar);
                        p(cal, ['initNamespace', namespace]);
                    } else p(cal, ar);
                    return;
                }
                p(cal, ar);
            };
    })(window, 'https://app.cal.com/embed/embed.js', 'init');
}

export function initNamespace(namespace, options = { origin: 'https://cal.com' }) {
    initCal(); // Ensures Cal global and script loading logic is in place

    const namespaceKey = `${namespace}-${options.origin}`;

    if (!initializedNamespaces.has(namespaceKey)) {
        window.Cal('init', namespace, options);
        initializedNamespaces.add(namespaceKey);
    }

    // The rest of the logic for ensuring Cal.ns[namespace] exists is handled by initCal's core logic
}

export function callNamespaceMethod(namespace, method, ...args) {
    if (window.Cal && window.Cal.ns && window.Cal.ns[namespace]) {
        window.Cal.ns[namespace](method, ...args);
    } else {
        // Queue it if the namespace isn't ready yet, or warn.
        // The original Cal script has a built-in queue, so this might also be handled if `initNamespace`
        // has been called and `embed.js` is just loading.
        // For robustness, you might consider adding a local queue here too or relying on Cal's internal one.
        // console.warn(`Namespace ${namespace} not fully initialized or Cal.com script not loaded yet. Attempting to queue call.`);
        // Fallback to global queue if namespace specific queue isn't set up by embed.js yet
        window.Cal(namespace, method, ...args);
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Cal.svelte - The Main Embed Wrapper

This Svelte 5 component uses Svelte 5 runes and handles all embed types based on the embedType prop. It renders a container for inline embeds and initializes the Cal API for all types.

  • It accepts props like embedType ('inline', 'floatingButton', or null for element-click), namespace, calLink, config, ui settings, and origin.
  • Using Svelte's onMount lifecycle hook, it initializes the Cal.com namespace and then calls the appropriate embed method ("inline" or "floatingButton") based on the embedType prop.
  • For inline embeds, it renders a div element that serves as the container for the embedded calendar, binding it to a $state variable for direct DOM manipulation. For other embed types, it renders nothing directly but handles the script initialization.
<!--
@component
A Svelte component to embed Cal.com scheduling links.
It wraps the Cal.com embed.js script and provides different embed types (inline, floating button).
It handles the initialization and API calls to the Cal.com script based on the provided props.

@param { 'inline' | 'floatingButton' | null } [embedType=null] - The type of Cal.com embed. Use 'inline' to embed directly in a div, 'floatingButton' for a popup button, or null for element-click (handled by CalLink.svelte).
@param { string } [namespace='15min'] - The namespace for the Cal.com embed instance. Useful if you have multiple embeds on the page.
@param { string } calLink - The Cal.com scheduling link (e.g., 'your-username/event-type'). Required for the embed to function.
@param { Record<string, any> } [config={ layout: 'month_view' }] - Configuration options for the Cal.com embed.
@param { Record<string, any> | null } [ui={ hideEventTypeDetails: false, layout: 'month_view' }] - UI customization options for the Cal.com embed.
@param { string } [origin='https://cal.com'] - The origin URL for the Cal.com embed.
@param { string } [class=''] - CSS class to apply to the container element (for inline type).
-->
<script lang="ts">
    import { initNamespace, callNamespaceMethod } from '$lib/cal.js';
    import { onMount } from 'svelte';

    let {
        embedType = null as 'inline' | 'floatingButton' | null,
        namespace = '15min',
        calLink = '',
        config = { layout: 'month_view' },
        ui = { hideEventTypeDetails: false, layout: 'month_view' } as Record<string, any> | null,
        origin = 'https://cal.com',
        class: className = ''
    } = $props();

    // Generate a unique ID for the inline container based on the namespace
    // This assumes namespaces are unique if multiple inline embeds are on the page.
    let divId: string = $derived(`cal-inline-container-${namespace}`);

    onMount(() => {
        // Initialize the namespace (idempotent thanks to changes in cal.js)
        initNamespace(namespace, { origin });

        if (embedType === 'inline') {
            // Ensure the div is actually in the DOM before trying to use it.
            // Svelte's rendering is synchronous with effect execution unless using $effect.pre or $effect.layout
            // For $effect, the DOM should be updated.
            // A small delay might be safer if Cal.com's script runs too quickly,
            // but ideally, its queuing handles elements not immediately present.
            // The selector method is generally more robust.
            callNamespaceMethod(namespace, 'inline', {
                elementOrSelector: `#${divId}`, // Use the selector string
                config,
                calLink
            });
        } else if (embedType === 'floatingButton') {
            callNamespaceMethod(namespace, 'floatingButton', { calLink, config });
        }

        // Configure UI (if ui prop is provided)
        // This will be called after 'inline' or 'floatingButton'
        if (ui) {
            callNamespaceMethod(namespace, 'ui', ui);
        }
    });
</script>

{#if embedType === 'inline'}
    <div id={divId} class={className} style="width:100%;height:100%;overflow:scroll">
    </div>
{/if}
Enter fullscreen mode Exit fullscreen mode

Practical Usage in Your SvelteKit App

Here's how you can easily integrate these components into your SvelteKit pages:

1. Inline Embed

Here's how to implement an inline embed in your SvelteKit page:

<script>
  import Cal from '$lib/Cal.svelte';
</script>

<div style="height: 600px; width: 100%;">
  <Cal embedType="inline" namespace="my-meeting" calLink="your-username/15min" />
</div>
Enter fullscreen mode Exit fullscreen mode

Note: Ensure the parent container of the Cal component for inline embeds has defined dimensions for the calendar to render correctly.

2. Floating Popup Embed

The floating popup embed adds a button that, when clicked, opens the calendar in a popup.

<script>
  import Cal from '$lib/Cal.svelte';
</script>

<Cal embedType="floatingButton" namespace="15min" calLink="your-username/15min" />
Enter fullscreen mode Exit fullscreen mode

3. Element-Click Embed

The element-click embed allows any element to trigger the calendar popup. With the updated Cal.svelte component, you can now directly use a standard HTML element with specific data-cal-* attributes to achieve this. The Cal component still needs to be present on the page to initialize the Cal.com script and namespace.

<script>
  import Cal from '$lib/Cal.svelte';
</script>

<Cal namespace="15min" /> 
<button
  data-cal-link="your-username/15min"
  data-cal-namespace="15min"
  class="px-6 py-3 bg-blue-600 text-white rounded-lg shadow-md hover:bg-blue-700 transition-colors"
>
  Book a 15min Meeting
</button>
Enter fullscreen mode Exit fullscreen mode

How It Works

Utility Module (cal.js)

  • initCal: Loads the Cal.com script only once, ensuring no duplicate script injections.
  • initNamespace: Initializes a namespace (e.g., "15min") with options like origin, avoiding redundant initializations.
  • callNamespaceMethod: Calls methods on the namespace (e.g., "inline", "floatingButton", "ui") with safety checks.

Main Embed Component (Cal.svelte)

  • Props: Uses $props() to define embedType, namespace, calLink, config, ui, and origin.
  • Lifecycle: On mount, it initializes the namespace and calls the appropriate method based on embedType.
  • Rendering: For inline, it renders a <div> and binds it to container for the calendar; for other types, it renders nothing.

Customization

You can customize the embeds by passing props:

  • Change the Namespace:
<Cal embedType="inline" namespace="30min" calLink="your-username/30min" />
Enter fullscreen mode Exit fullscreen mode
  • Adjust UI Settings:
<Cal
  embedType="floatingButton"
  namespace="15min"
  calLink="your-username/15min"
  ui={{ hideEventTypeDetails: true, layout: 'week_view' }}
/>
Enter fullscreen mode Exit fullscreen mode
  • Multiple Embeds: Use multiple instances with different namespaces:
<Cal embedType="inline" namespace="15min" calLink="your-username/15min" />
<Cal embedType="floatingButton" namespace="30min" calLink="your-username/30min" />
Enter fullscreen mode Exit fullscreen mode

Key Benefits of This Approach

  • Svelte 5 Runes Compliance: Fully embraces the new $props() and $state() runes, ensuring compatibility and leveraging Svelte 5's cutting-edge reactivity system for optimal performance and developer experience.
  • No Raw Scripts: Eliminates the need to scatter <script> tags directly in your Svelte components by encapsulating script loading and management within the cal.js utility, leading to cleaner, more maintainable, and secure code.
  • Modular and Reusable: Each component serves a specific purpose, making your codebase highly modular and the components easily reusable across your application.
  • Flexible and Customizable: Supports all Cal.com embed types with extensive customization options via component props.
  • Improved Developer Experience: Provides a Svelte-native way to interact with the Cal.com API, making integration feel like a natural extension of your SvelteKit app.

Ready to streamline your application's scheduling? Dive into the code examples and start building your integrated Cal.com experience today! For more advanced configurations, refer to the official Cal.com embed documentation.

Top comments (5)

Collapse
 
nevodavid profile image
Nevo David

Pretty cool seeing how smooth this setup looks now - I remember when stuff like this was way messier.

Collapse
 
dev_michael profile image
Michael Amachree

I honestly hadn't used it before, but I had to migrate a codebase from svelte 4 to svelte 5 for a client, and this was one of the things they required changed, so I found a cleaner method to implement it in svelte 5.

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

Growth like this is always nice to see. Kinda makes me wonder - what keeps this momentum actually lasting? Like, beyond the first few releases.

Collapse
 
dev_michael profile image
Michael Amachree

In my opinion, the Svelte crew enjoys what they are doing and what it is developing into, as opposed to just following orders from superiors or doing it because they were instructed not to.

Collapse
 
multiples profile image
Multiples

Impressed with this app!!