From 3d6b990064787c2df52cbd8674a63e4bb20dd973 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Tue, 2 Jun 2020 09:10:08 -0700 Subject: [PATCH] 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 b77cf6111253..5f961d9e1605 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'; @@ -52,6 +52,7 @@ export default class Block { claim: Array; hydrate: Array; mount: Array; + bubble: Array; measure: Array; fix: Array; animate: Array; @@ -100,6 +101,7 @@ export default class Block { claim: [], hydrate: [], mount: [], + bubble: [], measure: [], fix: [], animate: [], @@ -292,12 +294,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} @@ -387,6 +404,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` @@ -428,6 +449,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; @@ -451,7 +473,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`); @@ -462,7 +484,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) { @@ -481,6 +503,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 157e186ea6eb..20ec317b3ac6 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 00f803bbbd48..2af5887cc763 100644 --- a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts +++ b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts @@ -391,13 +391,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 7d2a92fa1bce..fae0a6eadb4f 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; @@ -215,10 +216,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 a8e37e9632a3..9a64e60f68c1 100644 --- a/src/runtime/internal/lifecycle.ts +++ b/src/runtime/internal/lifecycle.ts @@ -51,14 +51,3 @@ export function setContext(key, context) { export function getContext(key) { 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)); - } -} \ No newline at end of file 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}