A Svelte action for intersection observer

npm i svelte-use-io · Complaint box: @dereknguyen10
  • A01

    Install

    npm i svelte-use-io
    # or yarn add svelte-use-io
    # or pnpm i svelte-use-io
    
  • A02

    Quick usage

    Passing directly to html elements:

    <script>
    	import { onDestroy } from 'svelte';
    	import { createObserver } from 'svelte-use-io';
    	const { observer } = createObserver();
    	const doStuff = ({ detail }) => console.log({ detail });
    	// { detail: IntersectionObserverEntry }
    </script>
    
    <ul>
    	{#each Array.from({ length: 6 }, (_, i) => i + 1) as i (i)}
    	<li use:observer on:intersecting="{doStuff}">Item {i}</li>
    	{/each}
    </ul>
    

    Passing to components:

    <!-- In outer components -->
    <ul>
    	{#each content as { _id, ...data } (_id)}
    	<section {observer} {data}></section>
    	{/each}
    </ul>
    
    <!-- In Section.svelte -->
    <section use:observer>
    	<div>...</div>
    </section>
    

    Listening only once:

    	<div use:observer={{ once: true }}></div>
    	<!-- or -->
    	<div use:observer data-io-once="true"></div>
    

    Other demos:

  • A03

    Configure

    #create_observer / createObserver

    createObserver accepts IO options, plus a callback on a single entry, and a visual toggle to show the root element's rootBound (give it a try! It's on the top menu bar of this page.)

    interface Options {
        root?: Element | Document | null;
        rootMargin?: string;
        threshold?: number | number[];
    	callback?: ({
            entry: IntersectionObserverEntry
            observer: IntersectionObserver
        }) => void;
    	showRootBound?: boolean;
    }
    
    interface Returns {
        observer: (node: HTMLElement) => void;
        io: IntersectionObserver
    }
    

    ⚠️ If you pass in a custom callback, you'll have to create your own custom events. To retain the default behavior, import defaultCallback & wrap around it:

    import { defaultCallback } from 'svelte-use-io';
    const customCallback = ({ entry, observer }) => {
    	doStuff(entry);
    	defaultCallback({ entry, observer }); // send `on:intersecting`, `on:unintersecting`
    };
    

    #observer

    This code

    <div use:observer={{ once: true }}>
    

    ...will observe the div only once. Note that if once is false and then set to true, div will be observed once again, once.

    #Clean up / disconnect()

    When an observed element is destroyed, it'll also be unobserved. The observer instance will be garbage-collected when it no longer observes anything and have no references (See this thread.)

    If you'd like to disconnect all observers manually:

    import { onDestroy } from 'svelte';
    
    const { observer, io } = createObserver();
    
    onDestroy(() => {
    	io.disconnect();
    });
    
  • A04

    Typescript

    To prevent type error when adding custom event listeners on HTML elements, Add these to a definition file such as global.d.ts:

    declare namespace svelte.JSX {
    	export interface HTMLAttributes<T> {
    		onintersecting?: () => void;
    		onunintersecting?: () => void;
    	}
    }
    
  • A05

    Why

    Admittedly, the Intersection Observer API ('IO' from here on) is not difficult to use — but it is verbose and I have to look it up every time. This is a Svelte action that I've been copying from project to project & thought it's time to slab a few tests on it & publish as a package.

    I think the IO API is a lot easier to handle as events on elements vs. the callback API:

    <script>
        import { createObserver } from 'svelte-use-io'
        const { observer } = createObserver()
        let intersecting = false
    </script>
    
    <div
        use:observer
        on:intersecting={() => (intersecting = true)}
        on:unintersecting={() => (intersecting = false)}
    >
      <!-- ... -->
    </div>
    

    I'm also tempted to create a <Observer> component that may look something like this:

    // ⚠️ This component doesn't exists
    <Observer bind:intersecting>
    	<div>{/*...*/}</div>
    </Observer>
    

    ...but then it'd need an extra html element. Should it be a div or a section? Is it ok to just spread ...$$props into it? Alternatively, I can do something like slot forwarding, but now that's just a different kind of boilerplate.

    Feedback, thoughts, PRs are all welcomed.

  • Enable showRootBound and scroll over this box
    <!-- Just empty space to pad the box up -->
Checkout my storybook-like project for Svelte Kit called `talenote`.
👋 Bye!