Mechanism for bubbling all events. Closes sveltejs/svelte#2837

pull/4599/head
Timothy Johnson 5 years ago committed by Antony Jones
parent 229a16d6e7
commit db8e9806f5

@ -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`);
}
}

@ -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<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[]>;
@ -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;
}
`);

@ -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})`;

@ -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');

@ -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);
};

@ -53,14 +53,3 @@ export function setContext<T>(key, context: T) {
export function getContext<T>(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));
}
}

@ -0,0 +1,7 @@
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
</script>
<button on:click='{() => dispatch("foo", { answer: 42 })}'>click me</button>

@ -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);
}
};

@ -0,0 +1,5 @@
<script>
import Widget from './Widget.svelte';
</script>
<Widget on:*/>

@ -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>
`);
}
};

@ -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}
Loading…
Cancel
Save