From db8e9806f582569e7804d6d1a446a1fa1e676208 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Tue, 2 Jun 2020 09:10:08 -0700 Subject: [PATCH 1/2] Mechanism for bubbling all events. Closes sveltejs/svelte#2837 --- src/compiler/compile/nodes/EventHandler.ts | 3 -- src/compiler/compile/render_dom/Block.ts | 37 +++++++++++++++---- .../wrappers/Element/EventHandler.ts | 21 +++++------ .../wrappers/InlineComponent/index.ts | 25 +++++++++---- src/runtime/internal/Component.ts | 6 +++ src/runtime/internal/lifecycle.ts | 11 ------ .../Widget.svelte | 7 ++++ .../_config.js | 18 +++++++++ .../main.svelte | 5 +++ .../event-handler-bubble-all/Button.svelte | 1 + .../event-handler-bubble-all/_config.js | 21 +++++++++++ .../event-handler-bubble-all/main.svelte | 11 ++++++ 12 files changed, 127 insertions(+), 39 deletions(-) create mode 100644 test/runtime/samples/event-handler-bubble-all-component/Widget.svelte create mode 100644 test/runtime/samples/event-handler-bubble-all-component/_config.js create mode 100644 test/runtime/samples/event-handler-bubble-all-component/main.svelte create mode 100644 test/runtime/samples/event-handler-bubble-all/Button.svelte create mode 100644 test/runtime/samples/event-handler-bubble-all/_config.js create mode 100644 test/runtime/samples/event-handler-bubble-all/main.svelte diff --git a/src/compiler/compile/nodes/EventHandler.ts b/src/compiler/compile/nodes/EventHandler.ts index 110542d0b8b8..e7aa0e34d300 100644 --- a/src/compiler/compile/nodes/EventHandler.ts +++ b/src/compiler/compile/nodes/EventHandler.ts @@ -1,7 +1,6 @@ import Node from './shared/Node'; import Expression from './shared/Expression'; import Component from '../Component'; -import { sanitize } from '../../utils/names'; import { Identifier } from 'estree'; export default class EventHandler extends Node { @@ -42,8 +41,6 @@ export default class EventHandler extends Node { } } } - } else { - this.handler_name = component.get_unique_name(`${sanitize(this.name)}_handler`); } } diff --git a/src/compiler/compile/render_dom/Block.ts b/src/compiler/compile/render_dom/Block.ts index 1562635d0794..cf44f476fd61 100644 --- a/src/compiler/compile/render_dom/Block.ts +++ b/src/compiler/compile/render_dom/Block.ts @@ -1,6 +1,6 @@ import Renderer from './Renderer'; import Wrapper from './wrappers/shared/Wrapper'; -import { b, x } from 'code-red'; +import { b, x, p } from 'code-red'; import { Node, Identifier, ArrayPattern } from 'estree'; import { is_head } from './wrappers/shared/is_head'; @@ -47,6 +47,7 @@ export default class Block { claim: Array; hydrate: Array; mount: Array; + bubble: Array; measure: Array; fix: Array; animate: Array; @@ -95,6 +96,7 @@ export default class Block { claim: [], hydrate: [], mount: [], + bubble: [], measure: [], fix: [], animate: [], @@ -287,12 +289,27 @@ export default class Block { }`; } + if (this.chunks.bubble.length > 0) { + const bubble_fns: Identifier = { + type: 'Identifier', + name: '#bubble_fns' + }; + this.add_variable(bubble_fns, x`[]`); + + properties.bubble = x`function #bubble(type, callback) { + const local_dispose = []; + const fn = () => { + ${this.chunks.bubble} + #dispose.push(...local_dispose); + } + if (#mounted) fn() + else ${bubble_fns}.push(fn); + return () => @run_all(local_dispose); + }`; + } + if (this.chunks.mount.length === 0) { properties.mount = noop; - } else if (this.event_listeners.length === 0) { - properties.mount = x`function #mount(#target, #anchor) { - ${this.chunks.mount} - }`; } else { properties.mount = x`function #mount(#target, #anchor) { ${this.chunks.mount} @@ -382,6 +399,10 @@ export default class Block { d: ${properties.destroy} }`; + if (properties.bubble) { + return_value.properties.push(p`b: ${properties.bubble}`); + } + const block = dev && this.get_unique_name('block'); const body = b` @@ -423,6 +444,7 @@ export default class Block { this.chunks.hydrate.length > 0 || this.chunks.claim.length > 0 || this.chunks.mount.length > 0 || + this.chunks.bubble.length > 0 || this.chunks.update.length > 0 || this.chunks.destroy.length > 0 || this.has_animation; @@ -446,7 +468,7 @@ export default class Block { } render_listeners(chunk: string = '') { - if (this.event_listeners.length > 0) { + if (this.event_listeners.length > 0 || this.chunks.bubble.length > 0) { this.add_variable({ type: 'Identifier', name: '#mounted' }); this.chunks.destroy.push(b`#mounted = false`); @@ -457,7 +479,7 @@ export default class Block { this.add_variable(dispose); - if (this.event_listeners.length === 1) { + if (this.event_listeners.length === 1 && this.chunks.bubble.length === 0) { this.chunks.mount.push( b` if (!#mounted) { @@ -476,6 +498,7 @@ export default class Block { ${dispose} = [ ${this.event_listeners} ]; + ${this.chunks.bubble.length > 0 && b`@run_all(#bubble_fns)`} #mounted = true; } `); diff --git a/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts b/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts index 2fa2e9291a83..0276d62cea3b 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts @@ -14,20 +14,10 @@ export default class EventHandlerWrapper { constructor(node: EventHandler, parent: Wrapper) { this.node = node; this.parent = parent; - - if (!node.expression) { - this.parent.renderer.add_to_context(node.handler_name.name); - - this.parent.renderer.component.partly_hoisted.push(b` - function ${node.handler_name.name}(event) { - @bubble($$self, event); - } - `); - } } get_snippet(block) { - const snippet = this.node.expression ? this.node.expression.manipulate(block) : block.renderer.reference(this.node.handler_name); + const snippet = this.node.expression.manipulate(block); if (this.node.reassigned) { block.maintain_context = true; @@ -37,6 +27,15 @@ export default class EventHandlerWrapper { } render(block: Block, target: string | Expression) { + if (!this.node.expression) { + if (this.node.name === "*") + block.chunks.bubble.push(b`local_dispose.push(@listen(${target}, type, callback))`); + else + block.chunks.bubble.push(b`if (type === "${this.node.name}") local_dispose.push(@listen(${target}, "${this.node.name}", callback));`); + + return; + } + let snippet = this.get_snippet(block); if (this.node.modifiers.has('preventDefault')) snippet = x`@prevent_default(${snippet})`; diff --git a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts index be3d6d146d97..96bc1274a91d 100644 --- a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts +++ b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts @@ -388,13 +388,24 @@ export default class InlineComponentWrapper extends Wrapper { return b`@binding_callbacks.push(() => @bind(${this.var}, '${binding.name}', ${id}));`; }); - const munged_handlers = this.node.handlers.map(handler => { - const event_handler = new EventHandler(handler, this); - let snippet = event_handler.get_snippet(block); - if (handler.modifiers.has('once')) snippet = x`@once(${snippet})`; - - return b`${name}.$on("${handler.name}", ${snippet});`; - }); + const munged_handlers = this.node.handlers + .filter(handler => { + if (handler.expression) return true; + + if (handler.name === "*") + block.chunks.bubble.push(b`local_dispose.push(${name}.$on(type, callback))`); + else + block.chunks.bubble.push(b`if (type === "${handler.name}") local_dispose.push(${name}.$on("${handler.name}", callback));`); + + return false; + }) + .map(handler => { + const event_handler = new EventHandler(handler, this); + let snippet = event_handler.get_snippet(block); + if (handler.modifiers.has('once')) snippet = x`@once(${snippet})`; + + return b`${name}.$on("${handler.name}", ${snippet});`; + }); if (this.node.name === 'svelte:component') { const switch_value = block.get_unique_name('switch_value'); diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts index 459a78031a04..1aa6d22ab60e 100644 --- a/src/runtime/internal/Component.ts +++ b/src/runtime/internal/Component.ts @@ -11,6 +11,7 @@ interface Fragment { /* claim */ l: (nodes: any) => void; /* hydrate */ h: () => void; /* mount */ m: (target: HTMLElement, anchor: any) => void; + /* bubble */ b?: (type: string, callback: Function) => Function; /* update */ p: (ctx: any, dirty: any) => void; /* measure */ r: () => void; /* fix */ f: () => void; @@ -222,10 +223,15 @@ export class SvelteComponent { } $on(type, callback) { + const dispose = this.$$.fragment + && this.$$.fragment.b + && this.$$.fragment.b(type, callback) + || noop; const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = [])); callbacks.push(callback); return () => { + dispose(); const index = callbacks.indexOf(callback); if (index !== -1) callbacks.splice(index, 1); }; diff --git a/src/runtime/internal/lifecycle.ts b/src/runtime/internal/lifecycle.ts index 9e65e59cbd1c..f5bc58588a96 100644 --- a/src/runtime/internal/lifecycle.ts +++ b/src/runtime/internal/lifecycle.ts @@ -53,14 +53,3 @@ export function setContext(key, context: T) { export function getContext(key): T { return get_current_component().$$.context.get(key); } - -// TODO figure out if we still want to support -// shorthand events, or if we want to implement -// a real bubbling mechanism -export function bubble(component, event) { - const callbacks = component.$$.callbacks[event.type]; - - if (callbacks) { - callbacks.slice().forEach(fn => fn(event)); - } -} diff --git a/test/runtime/samples/event-handler-bubble-all-component/Widget.svelte b/test/runtime/samples/event-handler-bubble-all-component/Widget.svelte new file mode 100644 index 000000000000..cde03db92bd6 --- /dev/null +++ b/test/runtime/samples/event-handler-bubble-all-component/Widget.svelte @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/test/runtime/samples/event-handler-bubble-all-component/_config.js b/test/runtime/samples/event-handler-bubble-all-component/_config.js new file mode 100644 index 000000000000..22704cf1c33c --- /dev/null +++ b/test/runtime/samples/event-handler-bubble-all-component/_config.js @@ -0,0 +1,18 @@ +export default { + html: ` + + `, + + test({ assert, component, target, window }) { + const button = target.querySelector('button'); + const event = new window.MouseEvent('click'); + + let answer; + component.$on('foo', event => { + answer = event.detail.answer; + }); + + button.dispatchEvent(event); + assert.equal(answer, 42); + } +}; diff --git a/test/runtime/samples/event-handler-bubble-all-component/main.svelte b/test/runtime/samples/event-handler-bubble-all-component/main.svelte new file mode 100644 index 000000000000..7baecd2bde88 --- /dev/null +++ b/test/runtime/samples/event-handler-bubble-all-component/main.svelte @@ -0,0 +1,5 @@ + + + diff --git a/test/runtime/samples/event-handler-bubble-all/Button.svelte b/test/runtime/samples/event-handler-bubble-all/Button.svelte new file mode 100644 index 000000000000..63012962222d --- /dev/null +++ b/test/runtime/samples/event-handler-bubble-all/Button.svelte @@ -0,0 +1 @@ + diff --git a/test/runtime/samples/event-handler-bubble-all/_config.js b/test/runtime/samples/event-handler-bubble-all/_config.js new file mode 100644 index 000000000000..3a5ac7a18b83 --- /dev/null +++ b/test/runtime/samples/event-handler-bubble-all/_config.js @@ -0,0 +1,21 @@ +export default { + html: ` + + `, + + async test({ assert, component, target, window }) { + const button = target.querySelector('button'); + const event = new window.MouseEvent('click'); + + await button.dispatchEvent(event); + assert.htmlEqual(target.innerHTML, ` + +

hello!

+ `); + + await button.dispatchEvent(event); + assert.htmlEqual(target.innerHTML, ` + + `); + } +}; diff --git a/test/runtime/samples/event-handler-bubble-all/main.svelte b/test/runtime/samples/event-handler-bubble-all/main.svelte new file mode 100644 index 000000000000..5c0c5e4e6da6 --- /dev/null +++ b/test/runtime/samples/event-handler-bubble-all/main.svelte @@ -0,0 +1,11 @@ + + + + +{#if visible} +

hello!

+{/if} From 4c5e31db4b23fa278b8d7676b9e377793f290512 Mon Sep 17 00:00:00 2001 From: Antony Jones Date: Wed, 30 Sep 2020 07:36:02 +0100 Subject: [PATCH 2/2] lint fixes --- .../compile/render_dom/wrappers/Element/EventHandler.ts | 2 +- .../compile/render_dom/wrappers/InlineComponent/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts b/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts index 0276d62cea3b..f87091fbfa5e 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts @@ -28,7 +28,7 @@ export default class EventHandlerWrapper { render(block: Block, target: string | Expression) { if (!this.node.expression) { - if (this.node.name === "*") + if (this.node.name === '*') block.chunks.bubble.push(b`local_dispose.push(@listen(${target}, type, callback))`); else block.chunks.bubble.push(b`if (type === "${this.node.name}") local_dispose.push(@listen(${target}, "${this.node.name}", callback));`); diff --git a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts index 96bc1274a91d..2168b33cc913 100644 --- a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts +++ b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts @@ -392,7 +392,7 @@ export default class InlineComponentWrapper extends Wrapper { .filter(handler => { if (handler.expression) return true; - if (handler.name === "*") + if (handler.name === '*') block.chunks.bubble.push(b`local_dispose.push(${name}.$on(type, callback))`); else block.chunks.bubble.push(b`if (type === "${handler.name}") local_dispose.push(${name}.$on("${handler.name}", callback));`);