Merge pull request #3836 from tanhauhau/tanhauhau/dynamic-event-handler

feat dynamic event handler
pull/3876/head
Rich Harris 5 years ago committed by GitHub
commit 60f7f79a70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,8 +1,6 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import Component from '../Component';
import { b, x } from 'code-red';
import Block from '../render_dom/Block';
import { sanitize } from '../../utils/names';
import { Identifier } from 'estree';
@ -14,6 +12,7 @@ export default class EventHandler extends Node {
handler_name: Identifier;
uses_context = false;
can_make_passive = false;
reassigned?: boolean;
constructor(component: Component, parent, template_scope, info) {
super(component, parent, template_scope, info);
@ -22,7 +21,7 @@ export default class EventHandler extends Node {
this.modifiers = new Set(info.modifiers);
if (info.expression) {
this.expression = new Expression(component, this, template_scope, info.expression, true);
this.expression = new Expression(component, this, template_scope, info.expression);
this.uses_context = this.expression.uses_context;
if (/FunctionExpression/.test(info.expression.type) && info.expression.params.length === 0) {
@ -42,34 +41,12 @@ export default class EventHandler extends Node {
if (node && (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration' || node.type === 'ArrowFunctionExpression') && node.params.length === 0) {
this.can_make_passive = true;
}
}
}
} else {
const id = component.get_unique_name(`${sanitize(this.name)}_handler`);
component.add_var({
name: id.name,
internal: true,
referenced: true
});
component.partly_hoisted.push(b`
function ${id}(event) {
@bubble($$self, event);
this.reassigned = component.var_lookup.get(info.expression.name).reassigned;
}
`);
this.handler_name = id;
}
} else {
this.handler_name = component.get_unique_name(`${sanitize(this.name)}_handler`);
}
// TODO move this? it is specific to render-dom
render(block: Block) {
if (this.expression) {
return this.expression.manipulate(block);
}
// this.component.add_reference(this.handler_name);
return x`#ctx.${this.handler_name}`;
}
}

@ -203,13 +203,11 @@ export default class Block {
}
add_variable(id: Identifier, init?: Node) {
this.variables.forEach(v => {
if (v.id.name === id.name) {
if (this.variables.has(id.name)) {
throw new Error(
`Variable '${id.name}' already initialised with a different value`
);
}
});
this.variables.set(id.name, { id, init });
}

@ -3,20 +3,23 @@ import Wrapper from './shared/Wrapper';
import { b } from 'code-red';
import Body from '../../nodes/Body';
import { Identifier } from 'estree';
import EventHandler from './Element/EventHandler';
export default class BodyWrapper extends Wrapper {
node: Body;
render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
this.node.handlers.forEach(handler => {
const snippet = handler.render(block);
this.node.handlers
.map(handler => new EventHandler(handler, this))
.forEach(handler => {
const snippet = handler.get_snippet(block);
block.chunks.init.push(b`
@_document.body.addEventListener("${handler.name}", ${snippet});
@_document.body.addEventListener("${handler.node.name}", ${snippet});
`);
block.chunks.destroy.push(b`
@_document.body.removeEventListener("${handler.name}", ${snippet});
@_document.body.removeEventListener("${handler.node.name}", ${snippet});
`);
});
}

@ -0,0 +1,69 @@
import EventHandler from '../../../nodes/EventHandler';
import Wrapper from '../shared/Wrapper';
import Block from '../../Block';
import { b, x, p } from 'code-red';
const TRUE = x`true`;
const FALSE = x`false`;
export default class EventHandlerWrapper {
node: EventHandler;
parent: Wrapper;
constructor(node: EventHandler, parent: Wrapper) {
this.node = node;
this.parent = parent;
if (!node.expression) {
this.parent.renderer.component.add_var({
name: node.handler_name.name,
internal: true,
referenced: true,
});
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) : x`#ctx.${this.node.handler_name}`;
if (this.node.reassigned) {
block.maintain_context = true;
return x`function () { ${snippet}.apply(this, arguments); }`;
}
return snippet;
}
render(block: Block, target: string) {
let snippet = this.get_snippet(block);
if (this.node.modifiers.has('preventDefault')) snippet = x`@prevent_default(${snippet})`;
if (this.node.modifiers.has('stopPropagation')) snippet = x`@stop_propagation(${snippet})`;
if (this.node.modifiers.has('self')) snippet = x`@self(${snippet})`;
const args = [];
const opts = ['passive', 'once', 'capture'].filter(mod => this.node.modifiers.has(mod));
if (opts.length) {
args.push((opts.length === 1 && opts[0] === 'capture')
? TRUE
: x`{ ${opts.map(opt => p`${opt}: true`)} }`);
} else if (block.renderer.options.dev) {
args.push(FALSE);
}
if (block.renderer.options.dev) {
args.push(this.node.modifiers.has('stopPropagation') ? TRUE : FALSE);
args.push(this.node.modifiers.has('preventDefault') ? TRUE : FALSE);
}
block.event_listeners.push(
x`@listen(${target}, "${this.node.name}", ${snippet}, ${args})`
);
}
}

@ -24,6 +24,7 @@ import bind_this from '../shared/bind_this';
import { changed } from '../shared/changed';
import { is_head } from '../shared/is_head';
import { Identifier } from 'estree';
import EventHandler from './EventHandler';
const events = [
{
@ -113,6 +114,7 @@ export default class ElementWrapper extends Wrapper {
fragment: FragmentWrapper;
attributes: AttributeWrapper[];
bindings: Binding[];
event_handlers: EventHandler[];
class_dependencies: string[];
slot_block: Block;
@ -194,6 +196,8 @@ export default class ElementWrapper extends Wrapper {
// e.g. <audio bind:paused bind:currentTime>
this.bindings = this.node.bindings.map(binding => new Binding(block, binding, this));
this.event_handlers = this.node.handlers.map(event_handler => new EventHandler(event_handler, this));
if (node.intro || node.outro) {
if (node.intro) block.add_intro(node.intro.is_local);
if (node.outro) block.add_outro(node.outro.is_local);
@ -643,7 +647,7 @@ export default class ElementWrapper extends Wrapper {
}
add_event_handlers(block: Block) {
add_event_handlers(block, this.var, this.node.handlers);
add_event_handlers(block, this.var, this.event_handlers);
}
add_transitions(

@ -16,6 +16,7 @@ import is_dynamic from '../shared/is_dynamic';
import bind_this from '../shared/bind_this';
import { changed } from '../shared/changed';
import { Node, Identifier, ObjectExpression } from 'estree';
import EventHandler from '../Element/EventHandler';
export default class InlineComponentWrapper extends Wrapper {
var: Identifier;
@ -365,7 +366,8 @@ export default class InlineComponentWrapper extends Wrapper {
});
const munged_handlers = this.node.handlers.map(handler => {
let snippet = handler.render(block);
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});`;

@ -8,6 +8,7 @@ import add_actions from './shared/add_actions';
import { changed } from './shared/changed';
import { Identifier } from 'estree';
import { TemplateNode } from '../../../interfaces';
import EventHandler from './Element/EventHandler';
const associated_events = {
innerWidth: 'resize',
@ -34,9 +35,11 @@ const readonly = new Set([
export default class WindowWrapper extends Wrapper {
node: Window;
handlers: EventHandler[];
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: TemplateNode) {
super(renderer, block, parent, node);
this.handlers = this.node.handlers.map(handler => new EventHandler(handler, this));
}
render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
@ -47,7 +50,7 @@ export default class WindowWrapper extends Wrapper {
const bindings: Record<string, string> = {};
add_actions(component, block, '@_window', this.node.actions);
add_event_handlers(block, '@_window', this.node.handlers);
add_event_handlers(block, '@_window', this.handlers);
this.node.bindings.forEach(binding => {
// in dev mode, throw if read-only values are written to

@ -1,39 +1,10 @@
import Block from '../../Block';
import EventHandler from '../../../nodes/EventHandler';
import { x, p } from 'code-red';
const TRUE = x`true`;
const FALSE = x`false`;
import EventHandler from '../Element/EventHandler';
export default function add_event_handlers(
block: Block,
target: string,
handlers: EventHandler[]
) {
handlers.forEach(handler => {
let snippet = handler.render(block);
if (handler.modifiers.has('preventDefault')) snippet = x`@prevent_default(${snippet})`;
if (handler.modifiers.has('stopPropagation')) snippet = x`@stop_propagation(${snippet})`;
if (handler.modifiers.has('self')) snippet = x`@self(${snippet})`;
const args = [];
const opts = ['passive', 'once', 'capture'].filter(mod => handler.modifiers.has(mod));
if (opts.length) {
args.push((opts.length === 1 && opts[0] === 'capture')
? TRUE
: x`{ ${opts.map(opt => p`${opt}: true`)} }`);
} else if (block.renderer.options.dev) {
args.push(FALSE);
}
if (block.renderer.options.dev) {
args.push(handler.modifiers.has('stopPropagation') ? TRUE : FALSE);
args.push(handler.modifiers.has('preventDefault') ? TRUE : FALSE);
}
block.event_listeners.push(
x`@listen(${target}, "${handler.name}", ${snippet}, ${args})`
);
});
handlers.forEach(handler => handler.render(block, target));
}

@ -0,0 +1,107 @@
import {
SvelteComponent,
append,
detach,
element,
init,
insert,
listen,
noop,
run_all,
safe_not_equal,
set_data,
space,
text
} from "svelte/internal";
function create_fragment(ctx) {
let p0;
let button0;
let t1;
let button1;
let t3;
let p1;
let t4;
let t5;
let button2;
let dispose;
return {
c() {
p0 = element("p");
button0 = element("button");
button0.textContent = "set handler 1";
t1 = space();
button1 = element("button");
button1.textContent = "set handler 2";
t3 = space();
p1 = element("p");
t4 = text(ctx.number);
t5 = space();
button2 = element("button");
button2.textContent = "click";
dispose = [
listen(button0, "click", ctx.updateHandler1),
listen(button1, "click", ctx.updateHandler2),
listen(button2, "click", function () {
ctx.clickHandler.apply(this, arguments);
})
];
},
m(target, anchor) {
insert(target, p0, anchor);
append(p0, button0);
append(p0, t1);
append(p0, button1);
insert(target, t3, anchor);
insert(target, p1, anchor);
append(p1, t4);
insert(target, t5, anchor);
insert(target, button2, anchor);
},
p(changed, new_ctx) {
ctx = new_ctx;
if (changed.number) set_data(t4, ctx.number);
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(p0);
if (detaching) detach(t3);
if (detaching) detach(p1);
if (detaching) detach(t5);
if (detaching) detach(button2);
run_all(dispose);
}
};
}
function instance($$self, $$props, $$invalidate) {
let clickHandler;
let number = 0;
function updateHandler1() {
$$invalidate("clickHandler", clickHandler = () => $$invalidate("number", number = 1));
}
function updateHandler2() {
$$invalidate("clickHandler", clickHandler = () => $$invalidate("number", number = 2));
}
return {
clickHandler,
number,
updateHandler1,
updateHandler2
};
}
class Component extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, {});
}
}
export default Component;

@ -0,0 +1,23 @@
<script>
let clickHandler;
let number = 0;
function updateHandler1(){
clickHandler = () => number = 1;
}
function updateHandler2(){
clickHandler = () => number = 2;
}
</script>
<p>
<button on:click={updateHandler1}>set handler 1</button>
<button on:click={updateHandler2}>set handler 2</button>
</p>
<p>{ number }</p>
<button on:click={clickHandler}>click</button>

@ -0,0 +1,18 @@
export default {
html: `
<button>update handler</button>
<button>0</button>
`,
async test({ assert, component, target, window }) {
const [updateButton, button] = target.querySelectorAll('button');
const event = new window.MouseEvent('click');
await button.dispatchEvent(event);
assert.equal(component.count, 1);
await updateButton.dispatchEvent(event);
await button.dispatchEvent(event);
assert.equal(component.count, 11);
}
};

@ -0,0 +1,11 @@
<script>
import Button from './Button.svelte';
export let count = 0;
let clickHandler = () => count += 1;
function updateHandler(){
clickHandler = () => count += 10;
}
</script>
<button on:click={updateHandler}>update handler</button>
<Button on:click={clickHandler}>{count}</Button>

@ -0,0 +1,18 @@
export default {
html: `
<button>update handler</button>
<button>0</button>
`,
async test({ assert, component, target, window }) {
const [updateButton, button] = target.querySelectorAll('button');
const event = new window.MouseEvent('click');
await updateButton.dispatchEvent(event);
await button.dispatchEvent(event);
assert.equal(component.count, 10);
await button.dispatchEvent(event);
assert.equal(component.count, 10);
}
};

@ -0,0 +1,11 @@
<script>
import Button from './Button.svelte';
export let count = 0;
let clickHandler = () => count += 1;
function updateHandler(){
clickHandler = () => count += 10;
}
</script>
<button on:click={updateHandler}>update handler</button>
<Button on:click|once={clickHandler}>{count}</Button>

@ -0,0 +1,50 @@
export default {
html: `
<p>
<button>set handler 1</button>
<button>set handler 2</button>
</p>
<p>0</p>
<button>click</button>
`,
async test({ assert, component, target, window }) {
const [updateButton1, updateButton2, button] = target.querySelectorAll(
'button'
);
const event = new window.MouseEvent('click');
await button.dispatchEvent(event);
assert.htmlEqual(target.innerHTML, `
<p>
<button>set handler 1</button>
<button>set handler 2</button>
</p>
<p>0</p>
<button>click</button>
`);
await updateButton1.dispatchEvent(event);
await button.dispatchEvent(event);
assert.htmlEqual(target.innerHTML, `
<p>
<button>set handler 1</button>
<button>set handler 2</button>
</p>
<p>1</p>
<button>click</button>
`);
await updateButton2.dispatchEvent(event);
await button.dispatchEvent(event);
assert.htmlEqual(target.innerHTML, `
<p>
<button>set handler 1</button>
<button>set handler 2</button>
</p>
<p>2</p>
<button>click</button>
`);
},
};

@ -0,0 +1,23 @@
<script>
let clickHandler;
let number = 0;
function updateHandler1(){
clickHandler = () => number = 1;
}
function updateHandler2(){
clickHandler = () => number = 2;
}
</script>
<p>
<button on:click={updateHandler1}>set handler 1</button>
<button on:click={updateHandler2}>set handler 2</button>
</p>
<p>{ number }</p>
<button on:click={clickHandler}>click</button>
Loading…
Cancel
Save