Skip to content

Commit

Permalink
Merge pull request #3836 from tanhauhau/tanhauhau/dynamic-event-handler
Browse files Browse the repository at this point in the history
feat dynamic event handler
  • Loading branch information
Rich-Harris committed Nov 6, 2019
2 parents deb5b68 + 9a77794 commit 60f7f79
Show file tree
Hide file tree
Showing 18 changed files with 368 additions and 78 deletions.
33 changes: 5 additions & 28 deletions src/compiler/compile/nodes/EventHandler.ts
Original file line number Diff line number Diff line change
@@ -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';

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

this.reassigned = component.var_lookup.get(info.expression.name).reassigned;
}
}
} 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.handler_name = id;
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}`;
}
}
12 changes: 5 additions & 7 deletions src/compiler/compile/render_dom/Block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,11 @@ export default class Block {
}

add_variable(id: Identifier, init?: Node) {
this.variables.forEach(v => {
if (v.id.name === id.name) {
throw new Error(
`Variable '${id.name}' already initialised with a different value`
);
}
});
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 });
}
Expand Down
21 changes: 12 additions & 9 deletions src/compiler/compile/render_dom/wrappers/Body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@ 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});
`);
block.chunks.init.push(b`
@_document.body.addEventListener("${handler.node.name}", ${snippet});
`);

block.chunks.destroy.push(b`
@_document.body.removeEventListener("${handler.name}", ${snippet});
`);
});
block.chunks.destroy.push(b`
@_document.body.removeEventListener("${handler.node.name}", ${snippet});
`);
});
}
}
69 changes: 69 additions & 0 deletions src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts
Original file line number Diff line number Diff line change
@@ -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})`
);
}
}
6 changes: 5 additions & 1 deletion src/compiler/compile/render_dom/wrappers/Element/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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});`;
Expand Down
5 changes: 4 additions & 1 deletion src/compiler/compile/render_dom/wrappers/Window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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) {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
Loading

0 comments on commit 60f7f79

Please sign in to comment.