Skip to content

$$events to expose events used in a component #8065

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
brandonmcconnell opened this issue Nov 30, 2022 · 2 comments
Closed

$$events to expose events used in a component #8065

brandonmcconnell opened this issue Nov 30, 2022 · 2 comments

Comments

@brandonmcconnell
Copy link
Contributor

brandonmcconnell commented Nov 30, 2022

Describe the problem

In some cases, it would be very helpful to know which events are declared within a component, especially when wanting to conditionally render a different element or content depending on which events are used.

There's not currently a native way to do this, at least during component initialization, so a common alternative is to also expose a prop indicating whether an event is used or to pass the event itself as a prop instead of using Svelte's magic on: event binding.

Describe the proposed solution

Since Svelte is a compiler, these events should already be exposed, so while the callbacks themselves might not yet be registered at that point, the event names and the count of how many times each event name is used for that component instance is used should already be available and could be exposed to the component via a reserved keyword like $$events.

<script>
  // No fancy logic needed here ✨
</script>

{#if $$events.click}
  Join today and get started.
  <button on:click>Sign up</button>
{/if}

The functionality I'm proposing here is already possible by hooking into get_current_component().$$.callbacks, but this only works after the component has finished mounting, where the event names are the object's keys with an array of their callback functions as each key's value.

get_current_component().$$.callbacks:

{
  click: [f(), f()],
  custom: [f()],
}

proposed $$events:

{
  click: 2,
  custom: 1,
}

…or if for some reason there's some gotcha to using the count instead of a boolean, the values could just be true for each, though there could be an advantage to including the count (maybe… maybe not 🤷🏻‍♂️).

Pseudo-implementation of proposed solution

— a polyfill more-or-less for $$events using get_current_component().$$.callbacks after mounting

For the sake of this example, I've mocked this up in a REPL using _$$events (since the $$ prefix is reserved):
https://svelte.dev/repl/f05fe3637b3c47a38dedcc303f75dd0c?version=3.53.1

This is the gist of the implementation (screenshot below):

<script>
  import { onMount } from 'svelte';
  import { get_current_component } from 'svelte/internal';

  let _$$events = {};

  const currentComponent = get_current_component();
  onMount(() => {
    _$$events = Object.fromEntries(
      Object.entries(currentComponent.$$.callbacks).map(
        ([event, { length: count }]) => {
          return [event, count];
        }
      )
    );
  });
</script>

{#if _$$events.eventName} Event-specific conditionally rendered content {/if}

screenshot of REPL

Here is another REPL I put together which demonstrates another—potentially more realistic—use case, using the presence of events to conditionally render buttons associated with each declared event (even with multiple handlers for some events):
https://svelte.dev/repl/be01c4b5a9df4295a9ddc1968532dab2?version=3.53.1

screenshot of REPL

Alternatives considered

I briefly mentioned this above as well, but some viable alternatives may be to…

  • expose a prop indicating whether an event is used

    <Component on:click={doSomething} showButton={true} />
  • pass the event itself as a prop instead of using Svelte's magic on: event binding

    <Component onClick={doSomething} />
  • hook into get_current_component().$$.callbacks after the component has mounted

    (the same implementation outlined in this proposal, but implemented manually as needed)

Importance

would make my life easier

@brandonmcconnell
Copy link
Contributor Author

brandonmcconnell commented Apr 11, 2023

Here's a simple real-world scenario where I might've used this if it were supported:

PrintlessLink.svelte

<script lang="ts">
  import { isPrint } from 'css-utils';
  const print = isPrint();

  export let href: string = '';
  export let openNewWindow = false;
  
  $: isButton = !href && $$events.click;
  $: noAction = !href && !$$events.click;
</script>

{#if $print || noAction}
  <div>
    <slot />
  </div>
{:else}
  <svelte:element
    this={isButton ? 'button' : 'a'}
    {href}
    type={isButton ? 'button' : undefined}
    on:click
    target={!isButton && openNewWindow ? '_blank' : undefined}
  >
    <slot />
  </svelte:element>
{/if}

Notably, $$events.click is not satisfied because the else block contains an element forwarding/using on:click, bit rather because the expression calling/building this component instance uses on:click, like this:

<PrintlessLink on:click={() => goToPage(id)}> Visit the page </PrintlessLink>

This aligns with how both $$slots and $$props work, where their values depend on which slots and props are used when creating the component instance from within the parent component.

@Rich-Harris
Copy link
Member

Events were overhauled in Svelte 5, so I'll close this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants