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>
on:* maybe?
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
onwhich 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
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?
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.
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.
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 separatelyI 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.
If anyone needs this functionality, I've made a primitive approach to forward all standard UI events, plus any others you specify:
You can see how it's used in the Button component:
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.
Here is how I think it should get implemented,
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). 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. 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. Few more advantages, https://gomakethings.com/why-event-delegation-is-a-better-way-to-listen-for-events-in-vanilla-js/
@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.
<child @click="clickHandler" @input="inputHandler" @whatever="whateverHandler" />
<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:
<Child
on:click="clickHandler"
on:input="inputHandler"
on:whatever="whateverHandler"
/>
<input on:$listeners />
Or the more familiar destructuring
<input { ...$listeners } />
If anyone needs this functionality, I've made a primitive approach to forward all standard UI events, plus any others you specify:
You can see how it's used in the Button component:
This is effectively the same as using a bunch of
on:eventdirectives 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.
@jerriclynsjohn How are you trying to use it?
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?
@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? :thinking:
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.
Looks like RedHatter is doing a great job to provide this functionality but it hasn't quite made it into mainline before a conflicting change appears. Is there an alternative method now exposed for this?
Most helpful comment
on:*maybe?