refactor component props with improved state & event handling

This commit is contained in:
Elijah Duffy
2026-04-07 15:34:22 -07:00
parent 4d9cd25b19
commit 8c57fd9d7b

View File

@@ -1,18 +1,22 @@
<!-- @component <!-- @component
The Jitsi component is a lightweight wrapper that manages loading the Jitsi Meet External API The Jitsi component is a lightweight wrapper that manages loading the Jitsi Meet External API
and joining a Jitsi meeting room. It accepts configuration options and provides a callback and joining a Jitsi meeting room. It accepts configuration options and several callbacks that
when the API is ready for use. allow you to respond to basic meeting events such as joining and leaving. For advanced event
handling, use the getAPI() method to interact with the Jitsi instance directly.
WARNING: Assume that ALL properties are NOT reactive. Due to the nature of the Jitsi Meet WARNING: While properties are reactive, in most cases changing them after the API has
External API, changing properties after initialization will NOT update the Jitsi instance. initialized will trigger a complete re-render of the component, which will destroy the
Instead, use the getAPI() method to interact with the Jitsi instance directly. existing Jitsi instance and interrupt any ongoing meetings. Use the getAPI() method to
interact with the Jitsi instance directly without causing a re-render.
--> -->
<script lang="ts"> <script lang="ts">
import { onMount, type Snippet } from 'svelte'; import { onMount, type Snippet } from 'svelte';
import type { import type {
JitsiMeetExternalAPIOptions, JitsiMeetExternalAPIOptions,
JitsiMeetExternalAPI JitsiMeetExternalAPI,
VideoConferenceJoinedEvent,
VideoConferenceLeftEvent
} from '$lib/jitsi-iframe-api.d.ts'; } from '$lib/jitsi-iframe-api.d.ts';
import type { ClassValue } from 'svelte/elements'; import type { ClassValue } from 'svelte/elements';
@@ -32,29 +36,59 @@ Instead, use the getAPI() method to interact with the Jitsi instance directly.
*/ */
class?: ClassValue; class?: ClassValue;
/** /**
* Children are rendered inside the Jitsi container until the API is initialized. * Optional snippet rendered until the API is initialized and the meeting
* is joined. Can be used to provide a loading indicator or placeholder content.
*/ */
children?: Snippet; loading?: Snippet;
/** Whether to hold loading state until the meeting is joined (default: false). */
holdLoadingUntilJoined?: boolean;
/**
* Optional snippet rendered after the API is initialized and the meeting is joined.
* Can be used to provide additional UI elements or controls related to the meeting.
*/
joined?: Snippet<[api: JitsiMeetExternalAPI]>;
/**
* Optional snippet rendered after the user has left the meeting. Can be used to
* provide a farewell message or other post-meeting content.
*/
left?: Snippet;
/** /**
* Callback function that is called when the Jitsi API is loaded. Can * Callback function that is called when the Jitsi API is loaded. Can
* be used to provide a loading indicator until the API is ready. * be used to provide a loading indicator until the API is ready.
* @param api The Jitsi Meet External API instance. * @param api The Jitsi Meet External API instance.
*/ */
onready?: (api: JitsiMeetExternalAPI) => void; onapiready?: (api: JitsiMeetExternalAPI) => void;
/** Callback triggered when the local user joins the meeting. */
onjoin?: (ev: VideoConferenceJoinedEvent) => void;
/** Callback triggered when the local user leaves the meeting. */
onleave?: (ev: VideoConferenceLeftEvent) => void;
} }
let { let {
domain = 'https://meet.jit.si', domain = 'https://meet.jit.si',
options, options,
class: classValue, class: classValue,
children, loading,
onready holdLoadingUntilJoined = false,
joined,
left,
onapiready,
onjoin,
onleave
}: Props = $props(); }: Props = $props();
const src = $derived(`${new URL('external_api.js', domain).toString()}`); const src = $derived(`${new URL('external_api.js', domain).toString()}`);
let container: HTMLDivElement; let container: HTMLDivElement;
let ready = $state(false); /** tracks whether the Jitsi API is ready */
let apiReady = $state(false);
/** tracks whether the local user has joined the meeting */
let joinedMeeting = $state(false);
/** tracks whether the local user has left the meeting */
let leftMeeting = $state(false);
/** whether to display the loading state */
const showLoading = $derived(holdLoadingUntilJoined ? !joinedMeeting : !apiReady);
let api = $state<JitsiMeetExternalAPI | null>(null); let api = $state<JitsiMeetExternalAPI | null>(null);
onMount(() => { onMount(() => {
@@ -64,13 +98,14 @@ Instead, use the getAPI() method to interact with the Jitsi instance directly.
} }
}); });
const onload = () => { /** handles Jitsi script load event */
const handleScriptLoad = () => {
if (typeof window.JitsiMeetExternalAPI === 'undefined') { if (typeof window.JitsiMeetExternalAPI === 'undefined') {
console.error('Jitsi Meet External API is not available on the window object.'); console.error('Jitsi Meet External API is not available on the window object.');
return; return;
} }
ready = true; apiReady = true;
// Initialize the Jitsi Meet External API // Initialize the Jitsi Meet External API
api = new window.JitsiMeetExternalAPI(new URL(domain).host, { api = new window.JitsiMeetExternalAPI(new URL(domain).host, {
...options, ...options,
@@ -80,8 +115,18 @@ Instead, use the getAPI() method to interact with the Jitsi instance directly.
console.error('Failed to initialize Jitsi Meet External API.'); console.error('Failed to initialize Jitsi Meet External API.');
return; return;
} }
// Call the onready callback if provided // Call the onapiready callback if provided
onready?.(api); onapiready?.(api);
// Attach event listeners for meeting join and leave events
api.addListener('videoConferenceJoined', (ev: VideoConferenceJoinedEvent) => {
joinedMeeting = true;
onjoin?.(ev);
});
api.addListener('videoConferenceLeft', (ev: VideoConferenceLeftEvent) => {
leftMeeting = true;
onleave?.(ev);
});
}; };
/** /**
@@ -94,11 +139,21 @@ Instead, use the getAPI() method to interact with the Jitsi instance directly.
</script> </script>
<svelte:head> <svelte:head>
<script {src} {onload}></script> <script {src} onload={handleScriptLoad}></script>
</svelte:head> </svelte:head>
<div id="jitsi-container" bind:this={container} class={[classValue]}> <div class={['relative', classValue]}>
{#if !ready} {#if showLoading}
{@render children?.()} <div class="absolute inset-0">{@render loading?.()}</div>
{/if} {/if}
{#if joinedMeeting && api && joined}
<div class="absolute inset-0 pointer-events-none">{@render joined(api)}</div>
{/if}
{#if leftMeeting}
<div class="absolute inset-0">{@render left?.()}</div>
{/if}
<div id="jitsi-container" bind:this={container} class={['w-full h-full min-w-0']}></div>
</div> </div>