npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vue-rless-modal

v0.0.2

Published

a simplre renderless component to promisify the dialog behaviour of components

Readme

Vue-Renderless-Modal

This is a renderless component that handles the modal logic in a Promise friendly way.

Installation

npm install vue-rless-modal

Use

To use the component, import it and pass as the modal template as the default slot.

Trigger the component programatically

You can use the exposed method show() to trigger the interaction. This method returns a Promise that resolves to which ever value is resolved from the interaction.


<script lang="ts" setup>
  import { ref } from 'vue';
  import RenderlessModal, { type slot_props } from '../src/RenderlessModal.vue';

  const $modal = ref<InstanceType<typeof RenderlessModal>>(null!);

  function handleClick() {
    const n : number = Math.ceil(Math.random() * 10);
    
    $modal.value.show<number,number>(n)
      .then(
        e => console.log(`${e ?? n} was the number!`),
        () => console.log(`${n} wasn't the number!`)
      );
  }
</script>

<template>
  <RenderlessModal ref="$modal" v-slot="{ data, resolve, reject } : slot_props<number>">
    <fieldset>
      <legend>is {{ data }} your number?</legend>
      <button @click="resolve(data - 1)">-1</button>
      <button @click="resolve()">yes</button>
      <button @click="resolve(data + 1)">+1</button>
      <button @click="reject">no!</button>
    </fieldset>
  </RenderlessModal>

  <div>
    <button @click="handleClick">ask it!</button>
  </div>
</template>

Trigger the component within the template

you can also access the show() hook within the control slot scoped slot props

<template>
  <RenderlessModal>
    <template v-slot="{ data, resolve, reject } : slot_props<number>">
      <dialog :ref="e => (e as HTMLDialogElement)?.showModal()" @close="reject">
        <form method="dialog" @submit.prevent="resolve">

          <p>selecciona tu numero ({{ data - 2 }} - {{data + 2}})</p>

          <div>
            <input name="val" :value="data" type="number" :max="data + 2" :min="data - 2" />
          </div>

          <p>si no lo hay, cancela</p>

          <div>
            <button type="submit">ok</button>
            <button type="reset">reset</button>
            <button type="button" @click="reject">cancel</button>
          </div>
        </form>
      </dialog>
    </template>

    <template #control="{ show }">
      <button @click="show">send it!</button>
    </template>

  </RenderlessModal>
</template>

This example, however, is bad because the result of the interaction is lost, so here's a more advance example that handles the show method's return value.


<template>
  <RenderlessModal>
    <template v-slot="{ data, resolve, reject } : slot_props<number>">
      <dialog :ref="e => (e as HTMLDialogElement)?.showModal()" @close="reject">
        <form method="dialog" @submit.prevent="resolve">

          <p>select your number ({{ data - 2 }} - {{data + 2}})</p>

          <div>
            <input name="val" :value="data" type="number" :max="data + 2" :min="data - 2" />
          </div>

          <p>if you can't, cancel</p>

          <div>
            <button type="submit">ok</button>
            <button type="reset">reset</button>
            <button type="button" @click="reject">cancel</button>
          </div>
        </form>
      </dialog>
    </template>

    <template #control="{ show }">
      <button @click="handleShow({ show })">send it!</button>
    </template>

  </RenderlessModal>
</template>
<script setup lang="ts">
  function handleShow({ show } : Pick<InstanceType<typeof RenderlessModal>, 'show'>) {
    show<Event>(Math.ceil(Math.random() * 10))
      .then(e => {
          const $data = new FormData(e.target as HTMLFormElement);
          console.log(`${$data.get("val")} is the number!`);
        },
        () => console.log(`no number!`)
      );
  }
</script>

You may see that this example uses <dialog/> component. This is because this component is completly renderless. If you like your content display above the rest of the content, you should manage it yourself. However, you can perfectly copy this example and adjust to your needs.

Guide

This component exposes the show method to pop the dialog into existance and returns a Promise that resolves to whatever the modal resolves (or rejects).

This component is not opinionated, which mean that the Element displayed is responsibility of the dev. Usually, a modal is displayed using the <dialog/> element but there may be cases when you don't want it, you'd rather display a confirmation form in place or use a different Element. Due to this, this component was made renderless.

Now, the component exposes the v-slot of the default slot as

type slot_props<D = any> = {
  resolve(e? : any) : void;
  reject() : void;

  data : D;
};

| Property | Type | description | |---|---|---| | resolve | (e?: any) => void | this is the hook that the default slot consume to resolve the interaction | | reject | () => void | this is the hook that the default slot consume to reject the interaction | | data | Data | this are the props that will be pased to the default slot (more of this later) |

the component's control slot exposes the v-slot as

type control_slot_props = { 
  show : (data? : D) => Promise<any>;
}

| Property | Type | description | |---|---|---| | show | (data?: D) => void | this is the hook that the control slot consume to trigger the interaction |

Common mistakes

this aproach is very flexible, which means it is easier to make mistakes; therefore, some suggestions you could use are:

  • The easiest way to use this component is with the exposed method show rather than the control slot
  • The component does not provide a native way to avoid multiple triggers. This is done in case you have modal that you'd like to use multiple times in the same page. In case you want to prevent multiple modals showing (blocking the trigger event), i'll leave an example at the end

Considerations

  • This is a pure renderless component, all styling and interaction flow should be manually defined.

Example: trigger one modal at a time


<script lang="ts" setup>

  // this composable prevents calling an async function (by ignoring it) until it's previous call has finished
  function useFixedFn<T extends unknown[]>(handler : (...args : T) => Promise<void>) {
    const busy = ref(false);
    const close = () => busy.value = false;

    return {
      fn(...args : T) {
        if(busy.value) return;
        busy.value = true;

        handler(...args).finally(close);
      },
      busy,
    }
  }

  const $modal = ref<InstanceType<typeof RenderlessModal>>(null!);

  const { fn : handleClick, busy } = useFixedFn(() => {
    const n : number = Math.ceil(Math.random() * 10);
    
    return $modal.value.show<number,number>(n)
      .then(
        e => console.log(`${e ?? n} was the number!`),
        () => console.log(`${n} wasn't the number!`)
      );
  });

</script>