Skip to content

Latest commit

 

History

History
302 lines (247 loc) · 10.4 KB

File metadata and controls

302 lines (247 loc) · 10.4 KB

Using OER Finder Plugin in Svelte

This guide covers Svelte-specific integration. For component properties, events, available adapters, key types, and operating modes, see Client Packages.

Installation

Ensure the GitHub package registry is configured (see Registry Setup), then install the web components plugin:

pnpm add @edufeed-org/oer-finder-plugin

For additional installation details (pnpm overrides, etc.), see Client Packages — Web Components Plugin.

Basic Usage (Server-Proxy Mode)

The recommended pattern is to slot <oer-list> and <oer-load-more> inside <oer-search>. Import the plugin at the top level and use bind:this to get element references. For the full list of component properties and events, see Component Properties and the event tables in each component's section.

<script lang="ts">
  import type {
    OerSearchResultEvent,
    OerSearchElement,
    OerListElement,
    OerCardClickEvent,
    SourceConfig,
    LoadMoreElement
  } from '@edufeed-org/oer-finder-plugin';
  import { onMount } from 'svelte';
  import '@edufeed-org/oer-finder-plugin';

  let searchElement: OerSearchElement;
  let listElement: OerListElement;
  let loadMoreElement: LoadMoreElement;

  // Configure available sources (checked: true sets the pre-selected sources)
  const sources: SourceConfig[] = [
    { id: 'nostr-amb-relay', label: 'Nostr AMB Relay', checked: true },
    { id: 'openverse', label: 'Openverse' },
    { id: 'arasaac', label: 'ARASAAC' },
    { id: 'rpi-virtuell', label: 'RPI-Virtuell' },
    { id: 'wikimedia', label: 'Wikimedia Commons' },
  ];

  onMount(() => {
    // Set sources as a JS property (not HTML attribute)
    searchElement.sources = sources;

    searchElement.addEventListener('search-loading', () => {
      listElement.loading = true;
      loadMoreElement.loading = true;
    });

    // Handle search results
    searchElement.addEventListener('search-results', (event: Event) => {
      const { data, meta } = (event as OerSearchResultEvent).detail;
      listElement.oers = data;
      listElement.loading = false;
      loadMoreElement.metadata = meta;
      loadMoreElement.loading = false;
    });

    // Handle search errors
    searchElement.addEventListener('search-error', (event: Event) => {
      const { error } = (event as CustomEvent<{ error: string }>).detail;
      listElement.oers = [];
      listElement.error = error;
      loadMoreElement.metadata = null;
      loadMoreElement.loading = false;
    });

    // Handle search cleared
    searchElement.addEventListener('search-cleared', () => {
      listElement.oers = [];
      listElement.error = null;
      listElement.loading = false;
      loadMoreElement.metadata = null;
      loadMoreElement.loading = false;
    });

    // Handle card clicks
    listElement.addEventListener('card-click', (event: Event) => {
      const { oer } = (event as OerCardClickEvent).detail;
      const url = oer.amb?.id;
      if (url) {
        window.open(String(url), '_blank', 'noopener,noreferrer');
      }
    });
  });
</script>

<oer-search
  bind:this={searchElement}
  api-url="https://your-api-url.com"
  language="en"
  page-size="20"
>
  <oer-list bind:this={listElement} language="en"></oer-list>
  <oer-load-more bind:this={loadMoreElement} language="en"></oer-load-more>
</oer-search>

Direct Client Mode Example

The component code is identical to the server-proxy example above with two differences: adapters must be registered at startup, and the api-url attribute is omitted. For adapter details, see Available Adapters.

1. Register adapters once at the top level of your app:

import { registerAllBuiltInAdapters } from '@edufeed-org/oer-finder-plugin/adapters';
registerAllBuiltInAdapters();

2. Provide baseUrl in the source config where required (e.g., nostr-amb-relay):

<script lang="ts">
  import type { SourceConfig } from '@edufeed-org/oer-finder-plugin';

  const sources: SourceConfig[] = [
    { id: 'openverse', label: 'Openverse', checked: true },
    { id: 'arasaac', label: 'ARASAAC' },
    { id: 'wikimedia', label: 'Wikimedia Commons', checked: true },
    { id: 'nostr-amb-relay', label: 'Nostr AMB Relay', baseUrl: 'wss://amb-relay.edufeed.org' },
    { id: 'rpi-virtuell', label: 'RPI-Virtuell' },
  ];
</script>

3. Render <oer-search> without api-url:

<oer-search bind:this={searchElement} language="en" page-size="20">
  <oer-list bind:this={listElement} language="en"></oer-list>
  <oer-load-more bind:this={loadMoreElement} language="en"></oer-load-more>
</oer-search>

All event handlers remain the same as the server-proxy example.

SvelteKit Usage

SvelteKit uses server-side rendering (SSR) by default. Since web components rely on browser APIs, the plugin must be imported dynamically inside onMount to avoid SSR errors.

This example shows a reusable SvelteKit component (Svelte 5 runes syntax) that uses direct-client mode with <oer-load-more> for infinite scrolling and CSS variable theming:

<script lang="ts">
  import type {
    OerSearchResultEvent,
    OerSearchElement,
    OerListElement,
    OerCardClickEvent,
    SourceConfig,
    LoadMoreElement
  } from '@edufeed-org/oer-finder-plugin';
  import { onMount } from 'svelte';

  interface Props {
    onSelect: (imageUrl: string) => void;
  }

  const language = $state('en');
  const { onSelect }: Props = $props();

  const availableSources: SourceConfig[] = [
    { id: 'arasaac', label: 'ARASAAC' },
    { id: 'openverse', label: 'Openverse', checked: true },
    { id: 'wikimedia', label: 'Wikimedia', checked: true },
    { id: 'nostr-amb-relay', label: 'Nostr AMB', checked: true, baseUrl: 'wss://amb-relay.edufeed.org' },
    { id: 'rpi-virtuell', label: 'RPI Virtuell' },
  ];

  let searchEl: OerSearchElement;
  let listEl: OerListElement;
  let loadMoreElement: LoadMoreElement;

  onMount(async () => {
    // Dynamically import the plugin only on the client side to avoid SSR issues
    await import('@edufeed-org/oer-finder-plugin');

    // Register adapters for direct-client mode (no api-url)
    const { registerAllBuiltInAdapters } = await import('@edufeed-org/oer-finder-plugin/adapters');
    registerAllBuiltInAdapters();

    // Set sources as a JS property (not HTML attribute)
    searchEl.sources = availableSources;

    searchEl?.addEventListener('search-loading', () => {
      listEl.loading = true;
      loadMoreElement.loading = true;
    });

    // Handle search results
    searchEl?.addEventListener('search-results', (e: Event) => {
      const customEvent = e as OerSearchResultEvent;
      listEl.oers = customEvent.detail.data;
      listEl.loading = false;
      loadMoreElement.metadata = customEvent.detail.meta;
      loadMoreElement.loading = false;
    });

    searchEl?.addEventListener('search-error', (e: Event) => {
      const customEvent = e as CustomEvent<{ error: string }>;
      listEl.oers = [];
      listEl.error = customEvent.detail.error;
      loadMoreElement.metadata = null;
      loadMoreElement.loading = false;
    });

    searchEl?.addEventListener('search-cleared', () => {
      listEl.oers = [];
      listEl.loading = false;
      listEl.error = null;
      loadMoreElement.metadata = null;
      loadMoreElement.loading = false;
    });

    // Handle card selection - extract image URL from extensions
    listEl?.addEventListener('card-click', (e: Event) => {
      const customEvent = e as OerCardClickEvent;
      const oer = customEvent.detail.oer;
      const imageUrl = oer.extensions?.images?.high || oer.extensions?.images?.medium || oer.extensions?.images?.small || oer.amb?.id;
      if (imageUrl) {
        onSelect(imageUrl);
      }
    });
  });
</script>

<div class="oer-picker-container">
  <oer-search
    bind:this={searchEl}
    language={language}
    locked-type="image"
    page-size={12}
  >
    <oer-list bind:this={listEl} {language}></oer-list>
    <oer-load-more bind:this={loadMoreElement} language={language}></oer-load-more>
  </oer-search>
</div>

<style>
  .oer-picker-container {
    --primary-color: var(--accent);
    --primary-hover-color: color-mix(in oklch, var(--accent) 85%, black);
    --secondary-color: var(--secondary);
    --background-card: var(--card);
    --background-form: var(--background);
    --background-input: var(--background);
    --text-primary: var(--foreground);
    --text-secondary: var(--foreground);
    --text-muted: var(--muted-foreground);
    --border-color: var(--border);
    --input-border-color: var(--border);
  }
</style>

Key SvelteKit Differences

Concern Svelte (SPA) SvelteKit (SSR)
Plugin import Top-level import '@edufeed-org/oer-finder-plugin' Dynamic await import(...) inside onMount
Adapter registration Top-level import and call Dynamic await import(...) inside onMount
Reactivity Svelte 4 let bindings or Svelte 5 runes Svelte 5 $state(), $props() runes
Component API Props via export let Props via $props() interface
SSR safety Not a concern Must guard all DOM access with onMount

SSR Considerations

  • Always use dynamic imports: The plugin registers custom elements on import, which requires the customElements browser API. A top-level import will crash during SSR.
  • Guard DOM access: All addEventListener calls and property assignments on element refs must be inside onMount.
  • Optional chaining: Use searchEl?.addEventListener(...) to guard against refs that may not yet be assigned.

CSS Theming

Map your application's design tokens to the plugin's CSS custom properties on a wrapper element:

<style>
  .oer-wrapper {
    --primary-color: #8b5cf6;
    --primary-hover-color: #7c3aed;
    --secondary-color: #ec4899;
    --background-card: #ffffff;
    --background-form: #f8f9fa;
    --background-input: #ffffff;
    --text-primary: #1a1a1a;
    --text-secondary: #4a4a4a;
    --text-muted: #9ca3af;
    --border-color: #e5e7eb;
    --input-border-color: #d1d5db;
  }
</style>

See Client Packages — Styling with CSS Variables for the full list of available properties.

Example

See edufeed-org/kanban-editor#38