Skip to content

Commit

Permalink
Mechanism for bubbling all events. Closes sveltejs#2837
Browse files Browse the repository at this point in the history
  • Loading branch information
Timothy Johnson committed Jun 2, 2020
1 parent e606a0c commit 3d6b990
Show file tree
Hide file tree
Showing 12 changed files with 127 additions and 39 deletions.
3 changes: 0 additions & 3 deletions src/compiler/compile/nodes/EventHandler.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -42,8 +41,6 @@ export default class EventHandler extends Node {
}
}
}
} else {
this.handler_name = component.get_unique_name(`${sanitize(this.name)}_handler`);
}
}

Expand Down
37 changes: 30 additions & 7 deletions src/compiler/compile/render_dom/Block.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -52,6 +52,7 @@ export default class Block {
claim: Array<Node | Node[]>;
hydrate: Array<Node | Node[]>;
mount: Array<Node | Node[]>;
bubble: Array<Node | Node[]>;
measure: Array<Node | Node[]>;
fix: Array<Node | Node[]>;
animate: Array<Node | Node[]>;
Expand Down Expand Up @@ -100,6 +101,7 @@ export default class Block {
claim: [],
hydrate: [],
mount: [],
bubble: [],
measure: [],
fix: [],
animate: [],
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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;
Expand All @@ -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`);

Expand All @@ -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) {
Expand All @@ -481,6 +503,7 @@ export default class Block {
${dispose} = [
${this.event_listeners}
];
${this.chunks.bubble.length > 0 && b`@run_all(#bubble_fns)`}
#mounted = true;
}
`);
Expand Down
21 changes: 10 additions & 11 deletions src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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})`;
Expand Down
25 changes: 18 additions & 7 deletions src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
6 changes: 6 additions & 0 deletions src/runtime/internal/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
};
Expand Down
11 changes: 0 additions & 11 deletions src/runtime/internal/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
</script>

<button on:click='{() => dispatch("foo", { answer: 42 })}'>click me</button>
18 changes: 18 additions & 0 deletions test/runtime/samples/event-handler-bubble-all-component/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export default {
html: `
<button>click me</button>
`,

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);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
import Widget from './Widget.svelte';
</script>

<Widget on:*/>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<button on:*>toggle</button>
21 changes: 21 additions & 0 deletions test/runtime/samples/event-handler-bubble-all/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export default {
html: `
<button>toggle</button>
`,

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, `
<button>toggle</button>
<p>hello!</p>
`);

await button.dispatchEvent(event);
assert.htmlEqual(target.innerHTML, `
<button>toggle</button>
`);
}
};
11 changes: 11 additions & 0 deletions test/runtime/samples/event-handler-bubble-all/main.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script>
import Button from './Button.svelte'
export let visible;
</script>

<Button on:click='{() => visible = !visible}'>toggle</Button>

{#if visible}
<p>hello!</p>
{/if}

0 comments on commit 3d6b990

Please sign in to comment.