DEV Community

loading...
Cover image for How to use Popper with Svelte, in a Sveltish way, with Actions!

How to use Popper with Svelte, in a Sveltish way, with Actions!

gokayokyay profile image Gökay Okyay ・3 min read

What is Popper?

Popper identifies itself as a TOOLTIP & POPOVER POSITIONING ENGINE. It basically helps your popovers and tooltips position properly. Popper is awesome and used by millions and giants (according to their website) such as Microsoft, Atlassian, GitLab, etc. And I use it at work and while working on my side projects.

Okay, but with Svelte we can use any tools or libraries without an extra work or binding

Yes but it doesn't mean that we can't improve our codebase.

Yep, you're right, so how to use it?

First, I want to show you how you can use it without Sveltish way. Let's create a new Svelte project with:

npx degit sveltejs/template my-svelte-project
cd sveltishpopper
npm i
Enter fullscreen mode Exit fullscreen mode

Then install Popper

npm i @popperjs/core
Enter fullscreen mode Exit fullscreen mode

Then open src/App.svelte with your favorite editor, and delete everything.

Create a file named Modal.svelte, then paste following:

<style>
  #modal {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 40vh;
    width: 20vw;
    flex-direction: column;
    background-color: wheat;
    border-radius: 1em;
  }
  #modal > img {
    margin-top: 3em;
  }
</style>
<div id="modal">
  Hello Popper!
  <img src="https://popper.js.org/static/popper-logo-394b4ea5914aad7fc580f418ed0cfb17.svg" alt="Popper logo">
</div>
Enter fullscreen mode Exit fullscreen mode

It's just a basic modal with a background color.

Now open App.svelte and paste

<script>
    import { onMount } from 'svelte';
    import { createPopper } from '@popperjs/core/dist/esm';
    import Modal from './Modal.svelte';
    let popButton, modal, isModalActive = false;
    const toggleModal = () => {
        isModalActive = !isModalActive;
    };
    onMount(() => {
        createPopper(popButton, modal);
    });
</script>

<button bind:this={popButton} on:click={toggleModal}>
    Pop it
</button>
{#if isModalActive}
    <Modal bind:this={modal} />
{/if}
Enter fullscreen mode Exit fullscreen mode

In the code above, nothing really challenging goes on
And congratz! You got yourself a functioning modal with awesome positioning.
bad popper
This implementation is really simple but everytime you want to use a modal with popper, you need to repeat this implementation. How can we improve?

Action!

Action gif

Now create a file named, actually it doesn't really matter because this isn't really a project but let's call it, PopperAction.js.

And paste following:

import { createPopper } from '@popperjs/core/dist/esm';

export function popover(node, { component, ...props }) {
  const button = node;
  let popperInstance, componentInstance, renderedComponent, isActive = false;
  const id = 'modal';

  const toggle = e => {
    e.stopPropagation()
    isActive ? hide() : show();
  };

  button.addEventListener('click', toggle);

  const detectClickOutside = event => {
    if (renderedComponent && !renderedComponent.contains(event.target) && isActive) {
      hide();
    }
  };

  const show = () => {
    componentInstance = new component({
      target: document.body,
      props
    });
    isActive = true;
    renderedComponent = document.querySelector(`#${id}`);

    popperInstance = createPopper(button, renderedComponent, {
      modifiers: [
        {
          name: 'offset',
          options: {
            offset: [0, 8]
          }
        }
      ]
    });

    document.addEventListener('click', detectClickOutside);
  };

  const hide = () => {
    renderedComponent = document.querySelector(`#${id}`);
    isActive = false;
    if (popperInstance) {
      popperInstance.destroy();
      popperInstance = null;
    }
    componentInstance.$destroy();
    document.removeEventListener('click', detectClickOutside);
  }

  return {
    destroy() {
      button.removeEventListener('click', toggle);
      document.removeEventListener('click', detectClickOutside);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Okay, what the heck??

what the ...
This implementation is more confusing but let me explain what's happening down there. (You can skip if you understood what it does.)

So, we are defining a function that takes, a node and some props. We add a click event listener to the node (in this case its a button) and we bind it to a toggle function, which toggles the modal (eh?).

Show function is creating a popper instance every time we click to the button, and hide is hiding the modal and destroying the popper instance. You can figure out the optimizations yourselves, I'm in a rush!

As a bonus I ended up adding a click outside handler which detects clicks that are outside of the modal.

We return an object from the action.

  return {
    destroy() {
      button.removeEventListener('click', toggle);
      document.removeEventListener('click', detectClickOutside);
    }
  }
Enter fullscreen mode Exit fullscreen mode

It has a special method named destroy, it's duty is to clean up effects ( :) ).

Hmm, seems legit, but how to use it?

seems legit

That's the awesome part. Get ready to shocked in 3, 2, 1...
Carbon last

<script>
    import { popover } from './PopperAction.js';
    import Modal from './Modal.svelte'; 
</script>

<button use:popover={{ component: Modal }}>
    Pop it
</button>
Enter fullscreen mode Exit fullscreen mode

Look how beautiful it is. 😢

Okay this is it. Have a nice day.

PS, checkout official svelte package of popper

Discussion

pic
Editor guide