-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Comments
|
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:
|
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' |
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... 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 There are a few different things going on here:
|
I just realized that |
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 Edit: |
Here is how I think it should get implemented,
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. Parent.vue<child @click="clickHandler" @input="inputHandler" @whatever="whateverHandler" /> Child.vue<input v-on="$listeners" /> The above 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 } /> |
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 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 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 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 <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 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 Any idea of what I'm missing? 🤔 |
If all <button use:eventForwarding={$$on}>
<slot/>
</button> I wrote a little workaround demo, putting the event handlers on the |
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 |
I see that this conversation kind of fell off. Is this a feature that has been implemented? |
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 :
<script>
export let followMouse = false;
function myMouseHandler(evt) {
/* ... */
}
</script>
<button on:mousemove={followMouse && myMouseHandler}> click </button>
<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>
<button on:focus on:blur on:click on:dblclick on:mousemove on:touchmove ...> click </button>
What should we change ?Therefore, I am considering the following changes, which I have started to implement as a proof-of-concept :
<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? |
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? |
Actually I've some incorrect type-check on VS Code : Ex: Button.svelte : <button on:*><slot/></button> I have the following error on
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
In fact currently with I don't know how to fix this for now, but indeed it will have to be managed |
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 :
"svelte": "^3.54.0",
"svelte": "http://adiguba.com/svelte-3.55.1.tgz",
"acorn": "^8.8.2",
"css-tree": "^2.3.1"
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 WARNING !VS Code generate some syntaxic error on But above all the HMR is buggy : the event are not restored when code is reloaded. 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 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 |
Some upgrades with a new version : "svelte": "http://adiguba.com/svelte-3.55.2.tgz",
onEventListener() allow us to register an handler on the component.
<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> |
Some progress with this new version, which is "feature-complet" : "svelte": "http://adiguba.com/svelte-3.55.3.tgz",
<script>
export let followMouse = true;
function onMouseEnter() {
console.log("mouseEnter")
}
</script>
<div on:mouseenter={followMouse && onMouseEnter}>
...
</div> So the handler
Issues still to be fixed !
And I don't know how to fix that !!! |
I've updated my solution since I posted it here. It's available to use from the import { forwardEventsBuilder } from "@smui/common/internal";
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. |
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
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
|
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 |
I tried to solve this by preprocessor. What do you think?😆 GitHub repository |
Any update on this? |
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 |
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 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 |
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.tsimport 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);
};
} |
Already we have the feature in Svelte 5. For Svelte 3 or 4 users can use https://github.com/baseballyama/svelte-preprocess-delegate-events. |
Closing this issue - as pointed out above, event attributes replace the current |
An
on:
attribute, or something similar, would make it convenient to delegate all events from a component or html tag,The text was updated successfully, but these errors were encountered: