Skip to content
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

on:* attribute to delegate all events #2837

Closed
btakita opened this issue May 21, 2019 · 49 comments
Closed

on:* attribute to delegate all events #2837

btakita opened this issue May 21, 2019 · 49 comments
Labels
feature request popular more than 20 upthumbs
Milestone

Comments

@btakita
Copy link
Contributor

btakita commented May 21, 2019

An on: attribute, or something similar, would make it convenient to delegate all events from a component or html tag,

// All events on a are delegated through the component.
<a on:>My Link</a>
@ekhaled
Copy link
Contributor

ekhaled commented May 23, 2019

on:* maybe?

@btakita btakita changed the title on: attribute to delegate all events on:* attribute to delegate all events May 23, 2019
@Conduitry
Copy link
Member

Conduitry commented May 24, 2019

Anything that involves listening to all events on a DOM element isn't practical. There was a conversation in Discord between @mindrones and me about this on December 6, 2018, that you can read for more information. Some relevant quotes:

The best I thing I can find with a quick google for binding to all DOM events on an element is to iterate through all the methods on the element whose names begin with on which isn't too reliable

I don't see how to implement this feature for DOM events without an awful unreliable hack that I don't think Svelte wants to be responsible for

People can of course do this in their own code if they want, but I really don't think it should be an official feature

@mrkishi
Copy link
Member

mrkishi commented May 24, 2019

Yesterday on Discord, Tor brought up the idea of exposing an "event target" for a component. If you tried to add a listener on a component, it'd be delegated to that target instead.

I think this would be a fine syntax for that: instead of preemptively adding listeners to everything, each components' $on could either be the default or just a proxy to a child component's $on or child dom node's addEventListener. This could work, no?

@Conduitry
Copy link
Member

Oh that's interesting. So we wouldn't be forwarding all of them, just the ones that consumers of the component are trying to listen to. I don't see any technical DOM limitations getting in the way of that. There do still seem to be some questions though: Which events would attaching a handler attempt to proxy? all of them? Can we do this in a way that doesn't impact people not using it? Reopening to discuss this.

@Conduitry Conduitry reopened this May 24, 2019
@ekhaled
Copy link
Contributor

ekhaled commented May 24, 2019

just to add my 2 cents here...
In svelte 2 we could monkey-patch Component.prototype.fire to emulate this.
This allowed us to have a near-enough implementation of Higher Order Components.

Since the internals have changed in svelte 3... the only way we can have some semblance of HoCs.. is if we have compiler support.

@Conduitry
Copy link
Member

Monkey patching Component.prototype.fire only helped with listening to all events on a component, not on a DOM element.

There are a few different things going on here:

  • <element on:*> - this is the event target thing that @mrkishi mentioned. There are some details to iron out but it seems possible.
  • <element on:*={handler}> - this is not possible to implement in a sane way
  • <Component on:*> - probably possible, using a similar event target thing, but for component events
  • <Component on:*={handler}> - probably also possible. The interface would actually be simpler than it was in v2, since we are now using CustomEvents which have a type field, and we don't need to pass the type and the event payload separately

@Conduitry
Copy link
Member

Conduitry commented May 24, 2019

I just realized that <Component on:*={handler}> where the component contains <element on:*> would have to have caveats about not being able to report proxied DOM events from that element. So I'm withdrawing my support for the handler syntax. I think just allowing the forwarding on:* on DOM elements and components makes sense.

@hperrin
Copy link
Contributor

hperrin commented Jul 29, 2019

If anyone needs this functionality, I've made a primitive approach to forward all standard UI events, plus any others you specify:

https://github.com/hperrin/svelte-material-ui/blob/273ded17c978ece3dd87f32a58dd9839e5c61325/components/forwardEvents.js

You can see how it's used in the Button component:

https://github.com/hperrin/svelte-material-ui/blob/273ded17c978ece3dd87f32a58dd9839e5c61325/components/button/index.svelte#L3

This is effectively the same as using a bunch of on:event directives with no handlers, but it will create less code. However, it uses internal Svelte features that may change without warning in the future.

Edit:
Updated on 2023-03-05 here: #2837 (comment)

@deviprsd
Copy link

Here is how I think it should get implemented,

  • The compiler will be able to scope on:* with it's component when it has been implemented, so it shouldn't be a big change.
  • EventHandler gets compiled to run-time listen(...). This function can be changed to a subscribe/unsubscribe pattern, that maps the node and it's modifiers in some kind of data structures (if possible use fragment scope ids as keys).
  • Every time there is a new event it gets bound to the document with some new run time function as handler that will delegate the event to the appropriate callbacks and it's modifiers are then applied.
  • The listen(...) will return an unsubscribe that will be called when the fragment is destroyed, it should be fine to remove an event if there are no handlers left for that event.
  • I think this presents better opportunities to implement a bubbling mechanism as well
  • Reduces number of attaching/de-attaching listeners, seems more likely to improve performance

Few more advantages, https://gomakethings.com/why-event-delegation-is-a-better-way-to-listen-for-events-in-vanilla-js/

@matyunya matyunya mentioned this issue Aug 4, 2019
@pbastowski
Copy link

pbastowski commented Aug 19, 2019

@Conduitry Perhaps this was already discussed somewhere, but I haven't seen it, so, here is how I handle event forwarding in VueJs. I believe this could be adapted to Svelte.

I am omitting some template boilerplate for brevity.

Parent.vue

<child @click="clickHandler" @input="inputHandler" @whatever="whateverHandler" />

Child.vue

<input v-on="$listeners" />

The above v-on="$listeners" code binds the in the child component to any events that the parent component is interested in, not all events. $listeners is passed into the child from the parent, which knows what events are interesting to it.

I don't know if passing an array of event listeners down to the child from the parent can be implemented in Svelte 3, but if it could be, then we could use a similar syntax, for instance:

Parent.svelte

<Child 
  on:click="clickHandler" 
  on:input="inputHandler" 
  on:whatever="whateverHandler" 
/>

Child.svelte

<input on:$listeners />

Or the more familiar destructuring

<input { ...$listeners } />

@jerriclynsjohn
Copy link

If anyone needs this functionality, I've made a primitive approach to forward all standard UI events, plus any others you specify:

https://github.com/hperrin/svelte-material-ui/blob/273ded17c978ece3dd87f32a58dd9839e5c61325/components/forwardEvents.js

You can see how it's used in the Button component:

https://github.com/hperrin/svelte-material-ui/blob/273ded17c978ece3dd87f32a58dd9839e5c61325/components/button/index.svelte#L3

This is effectively the same as using a bunch of on:event directives with no handlers, but it will create less code. However, it uses internal Svelte features that may change without warning in the future.

I get error when i try to use this on components, says that I'm supposed to use it only on DOM elements.

@hperrin
Copy link
Contributor

hperrin commented Nov 8, 2019

@jerriclynsjohn How are you trying to use it?

@soullivaneuh
Copy link

The on:* attribute would be a great addition for tools like Storybook.

Indeed, to add some context on our component stories, we have to build some view components. Here is a use case with a view component adding a .container around the tested one:

The story:

import { action } from '@storybook/addon-actions';
import { decorators } from '../stories';
import Container from '../views/Container.svelte';
import MyComponent from './MyComponent.svelte';

export default {
  title: 'MyComponent',
  component: MyComponent,
  decorators,
};

export const Default = () => {
  return {
    Component: Container,
    props: {
      component: MyComponent,
      props: {},
    },
    on: {
      // Here we listen on validate event to show it on the Storybook dashboard.
      validate: action('validate'),
    },
  };
};

The tested component:

<script>
  import { createEventDispatcher } from 'svelte';
  const validate = () => {
    dispatch('validate');
  }
</script>

<Button on:click={validate}>
  Validate
</Button>

The container view:

<script>
  import { boolean } from '@storybook/addon-knobs';

  export let component;
  export let props;
  export let contained = boolean('container', true, 'layout');
</script>

<div class="{contained ? 'container' : ''}">
  <svelte:component this={component} {...props} on:* />
</div>

The on:* here would forward the event triggered by the child component.

Currently, I don't have any workaround to make event listening working on storybook with a view wrapper.

Do you have any state about this issue?

@soullivaneuh
Copy link

@hperrin I tried your workaround. It works very well for native browser events, but nor for custom events.

I directly downloaded your forwardEventsBuilder function and used it on this Container.svelte file:

<script>
  import { boolean } from '@storybook/addon-knobs';
  import {current_component} from 'svelte/internal';
  import { forwardEventsBuilder } from '../forwardEvents';

  const forwardEvents = forwardEventsBuilder(current_component, [
    // Custom event addition.
    'validate',
  ]);

  export let component;
  export let props;
  export let contained = boolean('container', true, 'layout');
</script>

<div class="{contained ? 'container' : ''}" use:forwardEvents>
  <svelte:component this={component} {...props} />
</div>

As you can see, I put the validate event on the builder events list argument.

Here is a simple test button component with a custom event dispatch:

<script>
  import { createEventDispatcher } from 'svelte';

  const dispatch = createEventDispatcher();
</script>

<button on:click={() => dispatch('validate') }>TEST</button>

And the related Storybook story:

import { action } from '@storybook/addon-actions';
import {
  text, boolean, select,
} from '@storybook/addon-knobs';
import Container from '../views/Container.svelte';
import Button from './Button.svelte';
import ButtonView from '../views/ButtonView.svelte';

export default {
  title: 'Button',
  component: Button,
  parameters: {
    notes: 'This is a button. You can click on it.',
  },
};

export const Default = () => ({
  // Container view usage.
  Component: Container,
  props: {
    // selve:component prop
    component: ButtonView,
    props: {},
  },
  on: {
    // Show the validate event trigger on the story board.
    validate: action('validate'),
  },
});

This is not working, but works with a native event like click.

Any idea of what I'm missing? 🤔

@brunnerh
Copy link
Member

If all on:... handlers were exposed on the component in a similar way to $$props, it would be very easy to implement event forwarding in client code by iterating over them. E.g. if there were an $$on object, the forwarding could be encapsulated in a use function, e.g.:

<button use:eventForwarding={$$on}>
    <slot/>
</button>

I wrote a little workaround demo, putting the event handlers on the $$props with prefix on-. Unfortunately this of course breaks the conventions and requires the component consumer to know about this mechanism.

@N00nDay
Copy link

N00nDay commented Oct 13, 2022

If anyone needs this functionality, I've made a primitive approach to forward all standard UI events, plus any others you specify:

https://github.com/hperrin/svelte-material-ui/blob/273ded17c978ece3dd87f32a58dd9839e5c61325/components/forwardEvents.js

You can see how it's used in the Button component:

https://github.com/hperrin/svelte-material-ui/blob/273ded17c978ece3dd87f32a58dd9839e5c61325/components/button/index.svelte#L3

This is effectively the same as using a bunch of on:event directives with no handlers, but it will create less code. However, it uses internal Svelte features that may change without warning in the future.

This works great except that I cannot figure out how to stop it from bubbling up and triggering the parent's actions. Because it is on a component I cannot do on:click| stopPropagation={...} as event modifiers can only be used on DOM elements, not components. Has anyone else encountered this and hopefully has a fix?

@Ddupasquier
Copy link

I see that this conversation kind of fell off. Is this a feature that has been implemented?
Or is it even in the works?

@adiguba
Copy link
Contributor

adiguba commented Mar 2, 2023

Hello,

I'm new, but I'm working on a rewrite of event handling, in order to handle on:* among other things.

Here the current behavior, and what I think should be changed :

  • Using on:click={handler} with a dynamic handler do no remove the listener when handler is null/undefined/invalid.
    The handler is just wrapped on a function that check if the handler is valid.
    In fack this roughly equivalent to on:click={(evt) => { if (typeof handler === 'function') handler(evt)}}.
    I think we should be able to remove the listener using something like that :
<script>
	export let followMouse = false;
	
	function myMouseHandler(evt) {
		/* ... */
	}
</script>

<button on:mousemove={followMouse && myMouseHandler}> click </button>
  • The on: directive on component only accept the once modifier.
    We cannot use the others event's modifiers (preventDefault, stopPropagation, passive, nonpassive, capture, self & trusted), event if we want to listen to a native event forwarded by the component.
    Same thing apply to the function $on() that miss the third parameters of addEventListener().

  • Actually when we want to forward an event (via on:click without a value), it add an event-listener on the DOM element that forward the event, even if no listener is present.
    This behavior could be problematic with an on:*, especially with "fast" events (like mouse/touch event) and complex to implements (how to determine all the events to add).
    Event forwarding should only use addEventListener() when a handler is added to the parent component (via .$on('click') or on:click={handler}).
    Actually on:click alway add an listener :

<script>
	import { createEventDispatcher } from 'svelte';
	const dispatch = createEventDispatcher();
</script>

<button on:click> click </button>
<!-- is roughly equivalent to -->
<button on:click={(evt)=>dispatch(evt.type, evt)}> click </button>
  • Because of its current implementation, we can forward an event with modifiers, like on:click|preventDefault.
    I don't see any rational use for this, and I think it should be forbidden and raise an error.
    The modifiers should be used when we add an handler, not when we forward an event.
    It's an breaking-change, but it will allow modifier to be used differently for event-forwarding (see after).

  • We cannot (easily) use alias for event-forwarding.
    For example if I have 2 buttons "ok" and "cancel", I want to fire two distinct events "ok-click" and "cancel-click"...

  • There is no way to forward all events from a node/component.
    We must specify each event one by one, which can be painful and inefficient (for the reasons mentioned above).
    And we do not necessarily know which events will be used by the caller...

<button on:focus on:blur on:click on:dblclick on:mousemove on:touchmove ...> click </button>
  • There no way to know when an event is suscribed on the component.
    This could be used for events that require external resources, which we would not want to use unnecessarily (for exemple for event from an EventSource or WebSockets...)

What should we change ?

Therefore, I am considering the following changes, which I have started to implement as a proof-of-concept :

  • on:click={handler} should remove the listener when handler is null/undefined/invalid.
  • on:click={handler} on component should accept all the event's modifier, and transmit this to forwarded events.
    In order to do that the $on() function should have a third argument like addEventListener.
  • Forwarding via on:click (without handler) must not add event-listener to the element/component.
    They will be added only when a handler is added by the parent component.
  • It should be illegal to use any event's modifier when forwarding an event via on:click (without handler).
    Using any of preventDefault, stopPropagation, passive, nonpassive, capture, self & trusted should generate a warning/error.
  • Instead when we forward an event, we should use modifier as an alias.
    Thus on:click|ok-click means that we forward the event 'click' under the alias-name 'ok-click'.
  • A new syntax like on:* (or on:%, or other to define...) should allow to forward all events to the element/component.
  • This new syntax should also make it possible to define aliases, so :
    • on:*|ok-* will forward all event by prefixing the name by 'ok-' ('click' will be forwarded as 'ok-click')
    • on:*|*-ok will forward all event by suffixing the name by '-ok' ('click' will be forwarded as 'click-ok')
      All other syntax should be illegal and raise an error (ex: on:*|label, on:*|*-label-*, on:*|prefix-*-suffix)
  • A new lifecycle function onEventListener() should allow use to handle some specific event.
    It will be called each time an event handler is added to the element (via on:event={handler} or $on('event', handler)), and will allow to register this handler.
    A dummy example:
<script>
    import { onEventListener } from 'svelte';

    // When a 'message' event is added to the component
    // We add it to the window :
    onEventListener('message', (eventType, handler, options) => {
        window.addEventListener(eventType, handler, options);
        return () => window.removeEventListener(eventType, handler, options);
    });
</script>

Do you see any other features that I forgot?
My implementation is almost finished, and I will post it here.

@YeungKC
Copy link

YeungKC commented Mar 2, 2023

@adiguba

I am excited about your comment, I made a portal component(https://github.com/YeungKC/svelte-portal).

There no way to forward events, so I can only pass various callback as parameters, it’s not elegant.

About the improvements, would Typescript type support be considered?

@adiguba
Copy link
Contributor

adiguba commented Mar 2, 2023

Actually I've some incorrect type-check on VS Code :

Ex: Button.svelte :

<button on:*><slot/></button>

I have the following error on on:* (but svelte-check is OK) :

Argument of type '{ "on:": undefined; }' is not assignable to parameter of type 'HTMLProps<"button", HTMLAttributes>'.
Object literal may only specify known properties, and '"on:
"' does not exist in type 'HTMLProps<"button", HTMLAttributes>'

Main.svelte :

<script lang="ts">
    import Button from "./Button.svelte";

    let count = 0;
    function increment(evt: MouseEvent) {
        count++;
        console.log("click on button " + evt.button);
    }
</script>

<Button on:click={increment}> {count} </Button>

I have the following error on on:click={increment} :

Argument of type '(evt: MouseEvent) => void' is not assignable to parameter of type '(e: CustomEvent) => void'.
Types of parameters 'evt' and 'e' are incompatible.
Type 'CustomEvent' is missing the following properties from type 'MouseEvent': altKey, button, buttons, clientX, and 20 more

In fact currently with on:*, all events are recognized as CustomEvent<any>.

I don't know how to fix this for now, but indeed it will have to be managed

@adiguba
Copy link
Contributor

adiguba commented Mar 2, 2023

My first working prototype is reading.

There are still a lot of work to do, but it's functional enough to get a good idea of what it would bring.

To install/test :

  1. Create a new svelte project : npm create svelte@latest myapp
  2. Edit the package.json and remove the standard svelte dependency :
"svelte": "^3.54.0",
  1. And replace it with the following lines :
"svelte": "http://adiguba.com/svelte-3.55.1.tgz",
"acorn": "^8.8.2",
"css-tree": "^2.3.1"
  1. Install : npm install
  2. Launch dev mode an enjoy : npm run dev

Note : I don't know why we need to include "acorn" and "css-tree" !?

Alternative : the source if forked here : https://github.com/adiguba/svelte/tree/on-any
You can clone it and compile.

WARNING !

VS Code generate some syntaxic error on on:*, and for now there are problems with type-checking and autocompletion !!

But above all the HMR is buggy : the event are not restored when code is reloaded.
I think this could be easy to fix, but for now you have to reload the page manually.

Some examples :

Unset the handler

<script>
    let count = 0;

    function increment() {
        count++;
    }
</script>

<button on:click={count < 10 ? increment : null}> count : {count}</button>

When the counter reaches 10, the handler is really removed from the DOM node.

All modifiers for component's on: directive

<!-- SvelteLink.svelte -->
<a on:click href="https://svelte.dev/">
    <slot/>
</a>
<script>
    import SvelteLink from "./SvelteLink.svelte";
</script>

<SvelteLink on:click|preventDefault={()=>window.alert("blocked")}>
    Svelte
</SvelteLink>

The link is inactive, thanks to the preventDefault modifier.

Forwarding an event don't add the listener until an handler is added.

<button on:click>
    click
</button>

We can verify via the browser's DevTools that no listener is added on the button until a handler is added to the component.

Event's modifiers are illegal when we forward and event.

<!-- ERROR : Forward-event only accept one modifier (the forward alias) -->
<button on:click|preventDefault|capture>
    click
</button>
<!-- WARNING: Forward-event only accept one modifier for the forward alias. Event modifiers should not be used here -->
<button on:click|preventDefault>
    click
</button>

Forward event with an alias name

<button on:click|buttonclick>
    click
</button>

WARNING : this is broken and don't work on this version... Will fix it...

Forward all events with on:*

<!-- Button.svelte -->
<button on:*>
    <slot/>
</button>
<script>
    import Button from "./Button.svelte";
    let infos = [];

    function logEvent(evt) {
        infos.push(evt.type);
        infos = infos;
    }
</script>


<Button on:click={logEvent} on:mouseenter={logEvent} on:mouseout={logEvent}>
    My button
</Button>
<ul>
{#each infos as i}
    <li>{i}</li>
{/each}
</ul>

All event handler added to the component Button will be added via addEventListener to the <button> element.

Forward all events with name alias

<!-- Box.svelte -->
<div>
    <p><slot/></p>

    <button on:*|cancel-*>Cancel</button>
    <button on:*|ok-*>Ok</button>
</div>

Each button uses a prefixed name for its events :

<script>
    import Box from "./Box.svelte";
</script>

<Box on:ok-click={()=>alert('OK')}
    on:cancel-click={()=>alert('Cancel')}
    on:click={()=>alert('Never called !')}>
    ...
</Box>

All event handler added to the component Button will be added via addEventListener to the <button> based on their prefix.
Other event handlers will simply be added to the component.

@adiguba
Copy link
Contributor

adiguba commented Mar 3, 2023

Some upgrades with a new version :

    "svelte": "http://adiguba.com/svelte-3.55.2.tgz",
  • Better errors/warnings
  • Forward event with an alias name is fixed.
    on:click|buttonclick will forward the event 'click' using the alias name 'buttonclick'.
  • Added a function onEventListener() which allows to manage event handler.

onEventListener() allow us to register an handler on the component.
It has 2 parameters :

  • The name of the event-type to process.
  • A function that register the handler, and return a dispose function.
<script>
   import { onEventListener } from "svelte";

   // When 'ws-message' is registered on this component
   onEventListener('ws-message', (handler) => {
       // Open the WebSocket :
       let socket = new WebSocket("wss://my-websocket-url/service");

       // Handle onmessage with the handler
       socket.onmessage = handler;

       // Handler is removed, closing connection
       return () => socket.close();
   });
   
</script>

@adiguba
Copy link
Contributor

adiguba commented Mar 4, 2023

Some progress with this new version, which is "feature-complet" :

  "svelte": "http://adiguba.com/svelte-3.55.3.tgz",
  • HMR is working correctly !!!

  • Improved the type for on:event={handler}
    Now handler can be false, to it will accept something like that :

<script>

    export let followMouse = true;

    function onMouseEnter() {
        console.log("mouseEnter")
    }


</script>

<div on:mouseenter={followMouse && onMouseEnter}>
    ...
</div>

So the handler onMouseEnter will be added/removed based on the followMouse boolean.

  • Added a definition for on:* in DOMAttributes

Issues still to be fixed !

  • The on:* directive is correctly accepted by npm run check, but it still produce an error on VS Code :

    Argument of type '{ "on:*": undefined; }' is not assignable to parameter of type 'HTMLProps<"button", HTMLAttributes>'.

  • Event forward via on:* or on:click|alias are not declared on the Component type, so there are not present on autocomplete and they can produce type error (as Svelte espect them to accept a CustomEvent<any> instead of the real type of the event).

And I don't know how to fix that !!!
If anyone has any idea or clue for this...

@hperrin
Copy link
Contributor

hperrin commented Mar 5, 2023

I've updated my solution since I posted it here. It's available to use from the @smui/common package.

import { forwardEventsBuilder } from "@smui/common/internal";
  • It forwards all events, not just UI events like my initial solution.
  • It supports all of the event modifiers (like capture, preventDefault, stopPropagation, etc).
  • It uses regular Svelte features, so you can use it with vanilla Svelte. (Kudos to @adiguba for an awesome and innovative solution, but it can't work in a UI library like SMUI unless it's merged into Svelte.)
  • You can use it on any element within your component.
  • It doesn't add unnecessary event listeners like my initial solution.

https://github.com/hperrin/svelte-material-ui/blob/master/packages/common/README.md#forwardeventsbuilder

npm install -D @smui/common
<!-- MyComponent.svelte -->
<div use:forwardEvents tabindex="0">
  <slot />
</div>

<script lang="ts">
  import { forwardEventsBuilder } from '@smui/common/internal';
  import { get_current_component } from 'svelte/internal';

  const forwardEvents = forwardEventsBuilder(get_current_component());
</script>
<MyComponent
  on:click={() => console.log('Click!')}
  on:mouseover={() => console.log('Mouseover!')}
  on:touchstart$passive={() => console.log("Touchstart, and it's passive!")}
  on:keypress$preventDefault$stopPropagation={() =>
    console.log('No key presses!')}
>
  Listen to my events!
</MyComponent>

<script lang="ts">
  import MyComponent from './MyComponent.svelte';
</script>

@N00nDay This fixes the issue you're having.

@brandonmcconnell
Copy link
Contributor

Totally different use case, but I suggested a feature a little while back that I think might pair well with this one in its general methodology: #8065

$$events a simple object similar to $$slots or $$props that exposes the events being listed to on a component, so you can dynamically load elements based on the presence of that event/handler.

The goal:

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

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

Today's workaround (relies on svelte/internal 🤷🏻‍♂️):

<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 }]) => [event, count]
    )
  ));
</script>

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

@phantomlsh
Copy link

phantomlsh commented Apr 3, 2023

Any progress here? I am looking forward to an elegant way to delegate all events, as it is critical for a UI component that might be used with undetermined events. And it is not satisfactory enough to use forwardEventsBuilder.

@baseballyama
Copy link
Member

I tried to solve this by preprocessor.

What do you think?😆

Play Ground
https://stackblitz.com/edit/sveltejs-kit-template-default-rwmhls?file=src%2Froutes%2F%2Bpage.svelte&terminal=dev

GitHub repository
https://github.com/baseballyama/svelte-preprocess-delegate-events

@mechadragon01
Copy link

Any update on this?

@brunnerh
Copy link
Member

The approach demonstrated by @baseballyama looks quite reasonable and non-hacky to me (which was one of the original concerns with this feature), so it would be nice if a version of that could be considered for direct integration into Svelte. That way there also would not be the need for a third-party with a dependency on svelte/internal.

@divmgl
Copy link

divmgl commented Sep 29, 2023

Running into this now. The use-case: I'm creating separate files with components that only have styling and view layer concerns, and I want to be able to pass in any DOM event into the component without having to define it ahead of time. Without this feature I have to manually list out every DOM event I want to support in the child component.

For example, here's a Button component:

<button on:click><slot /></button>

<style lang="postcss">
  button {
    @apply rounded bg-indigo-600 px-2 py-1 text-xs font-semibold text-white shadow-sm hover:bg-indigo-500;
    @apply focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600;
  }
</style>

It would be super cool if I could just forward all events from the parent using on:*.

@oMaN-Rod
Copy link

oMaN-Rod commented Dec 8, 2023

Stumbled across this issue in search of a solution, looking forward to native support in Svelte 5 but would be nice to have this in 4 as well. I'm using SkeletonUI/Tailwindcss and arrived at a solution based on this approach, thanks @hperrin! Has similar functionality, i.e.,

MyComponent.svelte, etc..
<script lang="ts">
	import { forwardEventsBuilder } from '$lib/utils/forwardEventsBuilder';
	import { current_component } from 'svelte/internal';

	const forwardEvents = forwardEventsBuilder(current_component);
</script>

<div use:forwardEvents>
	<slot />
</div>
+page.svelte
...

<Button on:click$once={() => console.log('This will only print once!')}>Click me</Button>
<MyComponent on:mouseout={() => console.log('where you going?')}>
	<Input
		bind:value
		on:mouseenter={() => console.log('here it comes, get ready')}
		on:input={() => console.log(value)}
		on:contextmenu={() => console.log('right click')}
	/>
	<Parent on:click={() => console.log('clicky it is')} />
</MyComponent>

...
forwardEventsBuilder.ts
import type { SvelteComponent } from 'svelte';

interface Modifiers {
	passive?: boolean;
	nonpassive?: boolean;
	capture?: boolean;
	once?: boolean;
	preventDefault?: boolean;
	stopPropagation?: boolean;
	stopImmediatePropagation?: boolean;
	self?: boolean;
	trusted?: boolean;
}

type EventCallback = (event: Event) => void;
type EventDestructor = () => void;

export function forwardEventsBuilder(component: SvelteComponent) {
	let $on: (eventType: string, callback: EventCallback) => EventDestructor;
	const events: [string, EventCallback][] = [];

	component.$on = (fullEventType: string, callback: EventCallback): EventDestructor => {
		let destructor = () => {};
		if ($on) {
			destructor = $on(fullEventType, callback);
		} else {
			events.push([fullEventType, callback]);
		}
		return destructor;
	};

	function bubble(e: Event) {
		const callbacks = component.$$.callbacks[e.type] as EventCallback[];
		if (callbacks) {
			callbacks.slice().forEach((fn: EventCallback) => fn.call(component, e));
		}
	}

	return (node: HTMLElement | SVGElement) => {
		const destructors: EventDestructor[] = [];
		const forwardDestructors: { [k: string]: EventDestructor } = {};

		$on = (fullEventType, callback) => {
			const { eventType, modifiers } = parseEventModifiers(fullEventType);
			let handler: EventListenerOrEventListenerObject = callback;
			const addEventListenerOptions: AddEventListenerOptions = {};

			if (modifiers.passive) {
				addEventListenerOptions.passive = true;
			}
			if (modifiers.nonpassive) {
				addEventListenerOptions.passive = false;
			}
			if (modifiers.capture) {
				addEventListenerOptions.capture = true;
			}
			if (modifiers.once) {
				addEventListenerOptions.once = true;
			}

			if (modifiers.preventDefault) {
				handler = prevent_default(handler as EventCallback);
			}
			if (modifiers.stopPropagation) {
				handler = stop_propagation(handler as EventCallback);
			}
			if (modifiers.stopImmediatePropagation) {
				handler = stop_immediate_propagation(handler as EventCallback);
			}
			if (modifiers.self) {
				handler = self_event(node, handler as EventCallback);
			}
			if (modifiers.trusted) {
				handler = trusted_event(handler as EventCallback);
			}

			const off = listen(node, eventType, handler, addEventListenerOptions);
			const destructor = () => {
				off();
				const idx = destructors.indexOf(destructor);
				if (idx > -1) {
					destructors.splice(idx, 1);
				}
			};

			destructors.push(destructor);

			if (!(eventType in forwardDestructors)) {
				forwardDestructors[eventType] = listen(node, eventType, bubble);
			}

			return destructor;
		};

		for (let i = 0; i < events.length; i++) {
			$on(events[i][0], events[i][1]);
		}

		return {
			destroy: () => {
				destructors.forEach((destructor) => destructor());
				Object.values(forwardDestructors).forEach((destructor) => destructor());
			}
		};
	};
}

function parseEventModifiers(fullEventType: string): { eventType: string; modifiers: Modifiers } {
	const parts = fullEventType.split('$');
	const eventType = parts[0];
	const eventModifiers = parts.slice(1);

	const modifiers: Modifiers = eventModifiers.reduce((acc, modifier) => {
		acc[modifier as keyof Modifiers] = true;
		return acc;
	}, {} as Modifiers);

	return { eventType, modifiers };
}

function listen(
	node: Node,
	event: string,
	handler: EventListenerOrEventListenerObject,
	options?: boolean | AddEventListenerOptions
): EventDestructor {
	node.addEventListener(event, handler, options);
	return () => node.removeEventListener(event, handler, options);
}

function prevent_default(fn: EventCallback): EventCallback {
	return function (this: SvelteComponent, event: Event) {
		event.preventDefault();
		return fn.call(this, event);
	};
}

function stop_propagation(fn: EventCallback): EventCallback {
	return function (this: SvelteComponent, event: Event) {
		event.stopPropagation();
		return fn.call(this, event);
	};
}

function stop_immediate_propagation(fn: EventCallback): EventCallback {
	return function (this: SvelteComponent, event: Event) {
		event.stopImmediatePropagation();
		return fn.call(this, event);
	};
}

function self_event(node: HTMLElement | SVGElement, fn: EventCallback): EventCallback {
	return function (this: SvelteComponent, event: Event) {
		if (event.target !== node) {
			return;
		}
		return fn.call(this, event);
	};
}

function trusted_event(fn: EventCallback): EventCallback {
	return function (this: SvelteComponent, event: Event) {
		if (!event.isTrusted) {
			return;
		}
		return fn.call(this, event);
	};
}

@baseballyama
Copy link
Member

Already we have the feature in Svelte 5.
https://svelte-5-preview.vercel.app/docs/event-handlers#bubbling-events

For Svelte 3 or 4 users can use https://github.com/baseballyama/svelte-preprocess-delegate-events.

@dummdidumm
Copy link
Member

Closing this issue - as pointed out above, event attributes replace the current on: system and allow for delegating all events.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request popular more than 20 upthumbs
Projects
None yet
Development

No branches or pull requests