Merge pull request #1247 from jacwright/behaviors

Adds actions to components
pull/1279/head
Rich Harris 8 years ago committed by GitHub
commit e77988b195
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -91,6 +91,7 @@ export default class Generator {
components: Set<string>;
events: Set<string>;
transitions: Set<string>;
actions: Set<string>;
importedComponents: Map<string, string>;
namespace: string;
hasComponents: boolean;
@ -134,6 +135,7 @@ export default class Generator {
this.components = new Set();
this.events = new Set();
this.transitions = new Set();
this.actions = new Set();
this.importedComponents = new Map();
this.slots = new Set();
@ -452,7 +454,7 @@ export default class Generator {
templateProperties[getName(prop.key)] = prop;
});
['helpers', 'events', 'components', 'transitions'].forEach(key => {
['helpers', 'events', 'components', 'transitions', 'actions'].forEach(key => {
if (templateProperties[key]) {
templateProperties[key].value.properties.forEach((prop: Node) => {
this[key].add(getName(prop.key));
@ -636,6 +638,12 @@ export default class Generator {
addDeclaration(getName(property.key), property.value, 'transitions');
});
}
if (templateProperties.actions) {
templateProperties.actions.value.properties.forEach((property: Node) => {
addDeclaration(getName(property.key), property.value, 'actions');
});
}
}
if (indentationLevel) {
@ -824,6 +832,16 @@ export default class Generator {
this.skip();
}
if (node.type === 'Action' && node.expression) {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
if (node.expression.type === 'CallExpression') {
node.expression.arguments.forEach((arg: Node) => {
arg.metadata = contextualise(arg, contextDependencies, indexes, true);
});
}
this.skip();
}
if (node.type === 'Component' && node.name === ':Component') {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
}

@ -0,0 +1,7 @@
import Node from './shared/Node';
export default class Action extends Node {
name: string;
value: Node[]
expression: Node
}

@ -12,13 +12,14 @@ import Binding from './Binding';
import EventHandler from './EventHandler';
import Ref from './Ref';
import Transition from './Transition';
import Action from './Action';
import Text from './Text';
import * as namespaces from '../../utils/namespaces';
export default class Element extends Node {
type: 'Element';
name: string;
attributes: (Attribute | Binding | EventHandler | Ref | Transition)[]; // TODO split these up sooner
attributes: (Attribute | Binding | EventHandler | Ref | Transition | Action)[]; // TODO split these up sooner
children: Node[];
init(
@ -84,6 +85,8 @@ export default class Element extends Node {
this.generator.hasOutroTransitions = block.hasOutroMethod = true;
block.outros += 1;
}
} else if (attribute.type === 'Action' && attribute.expression) {
block.addDependencies(attribute.metadata.dependencies);
}
}
});
@ -235,131 +238,11 @@ export default class Element extends Node {
}
this.addBindings(block, allUsedContexts);
this.attributes.filter((a: Attribute) => a.type === 'Attribute').forEach((attribute: Attribute) => {
attribute.render(block);
});
// event handlers
let eventHandlerUsesComponent = false;
this.attributes.filter((a: Node) => a.type === 'EventHandler').forEach((attribute: Node) => {
const isCustomEvent = generator.events.has(attribute.name);
const shouldHoist = !isCustomEvent && this.hasAncestor('EachBlock');
const context = shouldHoist ? null : name;
const usedContexts: string[] = [];
if (attribute.expression) {
generator.addSourcemapLocations(attribute.expression);
const flattened = flattenReference(attribute.expression.callee);
if (!validCalleeObjects.has(flattened.name)) {
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.code.prependRight(
attribute.expression.start,
`${block.alias('component')}.`
);
if (shouldHoist) eventHandlerUsesComponent = true; // this feels a bit hacky but it works!
}
attribute.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, context, true);
contexts.forEach(context => {
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
allUsedContexts.add(context);
});
});
}
const ctx = context || 'this';
const declarations = usedContexts
.map(name => {
if (name === 'state') {
if (shouldHoist) eventHandlerUsesComponent = true;
return `var state = ${block.alias('component')}.get();`;
}
const contextType = block.contextTypes.get(name);
if (contextType === 'each') {
const listName = block.listNames.get(name);
const indexName = block.indexNames.get(name);
const contextName = block.contexts.get(name);
return `var ${listName} = ${ctx}._svelte.${listName}, ${indexName} = ${ctx}._svelte.${indexName}, ${contextName} = ${listName}[${indexName}];`;
}
})
.filter(Boolean);
// get a name for the event handler that is globally unique
// if hoisted, locally unique otherwise
const handlerName = (shouldHoist ? generator : block).getUniqueName(
`${attribute.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
);
// create the handler body
const handlerBody = deindent`
${eventHandlerUsesComponent &&
`var ${block.alias('component')} = ${ctx}._svelte.component;`}
${declarations}
${attribute.expression ?
`[✂${attribute.expression.start}-${attribute.expression.end}✂];` :
`${block.alias('component')}.fire("${attribute.name}", event);`}
`;
if (isCustomEvent) {
block.addVariable(handlerName);
block.builders.hydrate.addBlock(deindent`
${handlerName} = %events-${attribute.name}.call(#component, ${name}, function(event) {
${handlerBody}
});
`);
block.builders.destroy.addLine(deindent`
${handlerName}.teardown();
`);
} else {
const handler = deindent`
function ${handlerName}(event) {
${handlerBody}
}
`;
if (shouldHoist) {
generator.blocks.push(handler);
} else {
block.builders.init.addBlock(handler);
}
block.builders.hydrate.addLine(
`@addListener(${name}, "${attribute.name}", ${handlerName});`
);
block.builders.destroy.addLine(
`@removeListener(${name}, "${attribute.name}", ${handlerName});`
);
}
});
// refs
this.attributes.filter((a: Node) => a.type === 'Ref').forEach((attribute: Node) => {
const ref = `#component.refs.${attribute.name}`;
block.builders.mount.addLine(
`${ref} = ${name};`
);
block.builders.destroy.addLine(
`if (${ref} === ${name}) ${ref} = null;`
);
generator.usesRefs = true; // so component.refs object is created
});
this.addAttributes(block);
const eventHandlerUsesComponent = this.addEventHandlers(block, allUsedContexts);
this.addRefs(block);
this.addTransitions(block);
this.addActions(block);
if (allUsedContexts.size || eventHandlerUsesComponent) {
const initialProps: string[] = [];
@ -548,6 +431,135 @@ export default class Element extends Node {
this.initialUpdate = mungedBindings.map(binding => binding.initialUpdate).filter(Boolean).join('\n');
}
addAttributes(block: Block) {
this.attributes.filter((a: Attribute) => a.type === 'Attribute').forEach((attribute: Attribute) => {
attribute.render(block);
});
}
addEventHandlers(block: Block, allUsedContexts) {
const { generator } = this;
let eventHandlerUsesComponent = false;
this.attributes.filter((a: EventHandler) => a.type === 'EventHandler').forEach((attribute: EventHandler) => {
const isCustomEvent = generator.events.has(attribute.name);
const shouldHoist = !isCustomEvent && this.hasAncestor('EachBlock');
const context = shouldHoist ? null : this.var;
const usedContexts: string[] = [];
if (attribute.expression) {
generator.addSourcemapLocations(attribute.expression);
const flattened = flattenReference(attribute.expression.callee);
if (!validCalleeObjects.has(flattened.name)) {
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.code.prependRight(
attribute.expression.start,
`${block.alias('component')}.`
);
if (shouldHoist) eventHandlerUsesComponent = true; // this feels a bit hacky but it works!
}
attribute.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, context, true);
contexts.forEach(context => {
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
allUsedContexts.add(context);
});
});
}
const ctx = context || 'this';
const declarations = usedContexts
.map(name => {
if (name === 'state') {
if (shouldHoist) eventHandlerUsesComponent = true;
return `var state = ${block.alias('component')}.get();`;
}
const contextType = block.contextTypes.get(name);
if (contextType === 'each') {
const listName = block.listNames.get(name);
const indexName = block.indexNames.get(name);
const contextName = block.contexts.get(name);
return `var ${listName} = ${ctx}._svelte.${listName}, ${indexName} = ${ctx}._svelte.${indexName}, ${contextName} = ${listName}[${indexName}];`;
}
})
.filter(Boolean);
// get a name for the event handler that is globally unique
// if hoisted, locally unique otherwise
const handlerName = (shouldHoist ? generator : block).getUniqueName(
`${attribute.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
);
// create the handler body
const handlerBody = deindent`
${eventHandlerUsesComponent &&
`var ${block.alias('component')} = ${ctx}._svelte.component;`}
${declarations}
${attribute.expression ?
`[✂${attribute.expression.start}-${attribute.expression.end}✂];` :
`${block.alias('component')}.fire("${attribute.name}", event);`}
`;
if (isCustomEvent) {
block.addVariable(handlerName);
block.builders.hydrate.addBlock(deindent`
${handlerName} = %events-${attribute.name}.call(#component, ${this.var}, function(event) {
${handlerBody}
});
`);
block.builders.destroy.addLine(deindent`
${handlerName}.teardown();
`);
} else {
const handler = deindent`
function ${handlerName}(event) {
${handlerBody}
}
`;
if (shouldHoist) {
generator.blocks.push(handler);
} else {
block.builders.init.addBlock(handler);
}
block.builders.hydrate.addLine(
`@addListener(${this.var}, "${attribute.name}", ${handlerName});`
);
block.builders.destroy.addLine(
`@removeListener(${this.var}, "${attribute.name}", ${handlerName});`
);
}
});
return eventHandlerUsesComponent;
}
addRefs(block: Block) {
this.attributes.filter((a: Ref) => a.type === 'Ref').forEach((attribute: Ref) => {
const ref = `#component.refs.${attribute.name}`;
block.builders.mount.addLine(
`${ref} = ${this.var};`
);
block.builders.destroy.addLine(
`if (${ref} === ${this.var}) ${ref} = null;`
);
this.generator.usesRefs = true; // so component.refs object is created
});
}
addTransitions(
block: Block
) {
@ -638,6 +650,45 @@ export default class Element extends Node {
}
}
addActions(block: Block) {
this.attributes.filter((a: Action) => a.type === 'Action').forEach((attribute:Action) => {
const { expression } = attribute;
let snippet, dependencies;
if (expression) {
this.generator.addSourcemapLocations(expression);
block.contextualise(expression);
snippet = attribute.metadata.snippet;
dependencies = attribute.metadata.dependencies;
}
const name = block.getUniqueName(
`${attribute.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_action`
);
block.addVariable(name);
const fn = `%actions-${attribute.name}`;
block.builders.hydrate.addLine(
`${name} = ${fn}(${this.var}${snippet ? `, ${snippet}` : ''}) || {};`
);
if (dependencies && dependencies.length) {
let conditional = `typeof ${name}.update === 'function' && `;
const deps = dependencies.map(dependency => `changed.${dependency}`).join(' || ');
conditional += dependencies.length > 1 ? `(${deps})` : deps;
block.builders.update.addConditional(
conditional,
`${name}.update(${snippet});`
);
}
block.builders.destroy.addLine(
`if (typeof ${name}.destroy === 'function') ${name}.destroy();`
);
});
}
getStaticAttributeValue(name: string) {
const attribute = this.attributes.find(
(attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name

@ -1,6 +1,7 @@
import Node from './shared/Node';
import Attribute from './Attribute';
import AwaitBlock from './AwaitBlock';
import Action from './Action';
import Binding from './Binding';
import CatchBlock from './CatchBlock';
import Comment from './Comment';
@ -26,6 +27,7 @@ import Window from './Window';
const nodes: Record<string, any> = {
Attribute,
AwaitBlock,
Action,
Binding,
CatchBlock,
Comment,

@ -49,7 +49,17 @@ const DIRECTIVES = {
},
allowedExpressionTypes: ['ObjectExpression'],
error: 'Transition argument must be an object literal, e.g. `{ duration: 400 }`'
}
},
Action: {
names: [ 'use' ],
attribute(start, end, type, name, expression) {
return { start, end, type, name, expression };
},
allowedExpressionTypes: [ 'Identifier', 'MemberExpression', 'ObjectExpression', 'Literal', 'CallExpression' ],
error: 'Data passed to actions must be an identifier (e.g. `foo`), a member expression ' +
'(e.g. `foo.bar` or `foo[baz]`), a method call (e.g. `foo()`), or a literal (e.g. `true` or `\'a string\'`'
},
};

@ -217,6 +217,19 @@ export default function validateElement(
if (attribute.name === 'slot' && !isComponent) {
checkSlotAttribute(validator, node, attribute, stack);
}
} else if (attribute.type === 'Action') {
if (isComponent) {
validator.error(`Actions can only be applied to DOM elements, not components`, attribute);
}
validator.used.actions.add(attribute.name);
if (!validator.actions.has(attribute.name)) {
validator.error(
`Missing action '${attribute.name}'`,
attribute
);
}
}
});
}

@ -33,6 +33,7 @@ export class Validator {
methods: Map<string, Node>;
helpers: Map<string, Node>;
transitions: Map<string, Node>;
actions: Map<string, Node>;
slots: Set<string>;
used: {
@ -40,6 +41,7 @@ export class Validator {
helpers: Set<string>;
events: Set<string>;
transitions: Set<string>;
actions: Set<string>;
};
constructor(parsed: Parsed, source: string, options: CompileOptions) {
@ -56,13 +58,15 @@ export class Validator {
this.methods = new Map();
this.helpers = new Map();
this.transitions = new Map();
this.actions = new Map();
this.slots = new Set();
this.used = {
components: new Set(),
helpers: new Set(),
events: new Set(),
transitions: new Set()
transitions: new Set(),
actions: new Set(),
};
}
@ -139,7 +143,8 @@ export default function validate(
// TODO helpers require a bit more work — need to analyse all expressions
// helpers: 'helper',
events: 'event definition',
transitions: 'transition'
transitions: 'transition',
actions: 'actions',
};
Object.keys(categories).forEach(category => {

@ -85,7 +85,7 @@ export default function validateJs(validator: Validator, js: Node) {
}
});
['components', 'methods', 'helpers', 'transitions'].forEach(key => {
['components', 'methods', 'helpers', 'transitions', 'actions'].forEach(key => {
if (validator.properties.has(key)) {
validator.properties.get(key).value.properties.forEach((prop: Node) => {
validator[key].set(getName(prop.key), prop.value);

@ -0,0 +1,16 @@
import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Validator } from '../../';
import { Node } from '../../../interfaces';
export default function actions(validator: Validator, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
validator.error(
`The 'actions' property must be an object literal`,
prop
);
}
checkForDupes(validator, prop.value.properties);
checkForComputedKeys(validator, prop.value.properties);
}

@ -1,4 +1,5 @@
import data from './data';
import actions from './actions';
import computed from './computed';
import oncreate from './oncreate';
import ondestroy from './ondestroy';
@ -19,6 +20,7 @@ import immutable from './immutable';
export default {
data,
actions,
computed,
oncreate,
ondestroy,

@ -0,0 +1,246 @@
function noop() {}
function assign(target) {
var k,
source,
i = 1,
len = arguments.length;
for (; i < len; i++) {
source = arguments[i];
for (k in source) target[k] = source[k];
}
return target;
}
function insertNode(node, target, anchor) {
target.insertBefore(node, anchor);
}
function detachNode(node) {
node.parentNode.removeChild(node);
}
function createElement(name) {
return document.createElement(name);
}
function blankObject() {
return Object.create(null);
}
function destroy(detach) {
this.destroy = noop;
this.fire('destroy');
this.set = this.get = noop;
if (detach !== false) this._fragment.u();
this._fragment.d();
this._fragment = this._state = null;
}
function _differs(a, b) {
return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function');
}
function dispatchObservers(component, group, changed, newState, oldState) {
for (var key in group) {
if (!changed[key]) continue;
var newValue = newState[key];
var oldValue = oldState[key];
var callbacks = group[key];
if (!callbacks) continue;
for (var i = 0; i < callbacks.length; i += 1) {
var callback = callbacks[i];
if (callback.__calling) continue;
callback.__calling = true;
callback.call(component, newValue, oldValue);
callback.__calling = false;
}
}
}
function fire(eventName, data) {
var handlers =
eventName in this._handlers && this._handlers[eventName].slice();
if (!handlers) return;
for (var i = 0; i < handlers.length; i += 1) {
handlers[i].call(this, data);
}
}
function get(key) {
return key ? this._state[key] : this._state;
}
function init(component, options) {
component._observers = { pre: blankObject(), post: blankObject() };
component._handlers = blankObject();
component._bind = options._bind;
component.options = options;
component.root = options.root || component;
component.store = component.root.store || options.store;
}
function observe(key, callback, options) {
var group = options && options.defer
? this._observers.post
: this._observers.pre;
(group[key] || (group[key] = [])).push(callback);
if (!options || options.init !== false) {
callback.__calling = true;
callback.call(this, this._state[key]);
callback.__calling = false;
}
return {
cancel: function() {
var index = group[key].indexOf(callback);
if (~index) group[key].splice(index, 1);
}
};
}
function on(eventName, handler) {
if (eventName === 'teardown') return this.on('destroy', handler);
var handlers = this._handlers[eventName] || (this._handlers[eventName] = []);
handlers.push(handler);
return {
cancel: function() {
var index = handlers.indexOf(handler);
if (~index) handlers.splice(index, 1);
}
};
}
function set(newState) {
this._set(assign({}, newState));
if (this.root._lock) return;
this.root._lock = true;
callAll(this.root._beforecreate);
callAll(this.root._oncreate);
callAll(this.root._aftercreate);
this.root._lock = false;
}
function _set(newState) {
var oldState = this._state,
changed = {},
dirty = false;
for (var key in newState) {
if (this._differs(newState[key], oldState[key])) changed[key] = dirty = true;
}
if (!dirty) return;
this._state = assign({}, oldState, newState);
this._recompute(changed, this._state);
if (this._bind) this._bind(changed, this._state);
if (this._fragment) {
dispatchObservers(this, this._observers.pre, changed, this._state, oldState);
this._fragment.p(changed, this._state);
dispatchObservers(this, this._observers.post, changed, this._state, oldState);
}
}
function callAll(fns) {
while (fns && fns.length) fns.shift()();
}
function _mount(target, anchor) {
this._fragment[this._fragment.i ? 'i' : 'm'](target, anchor || null);
}
function _unmount() {
if (this._fragment) this._fragment.u();
}
var proto = {
destroy: destroy,
get: get,
fire: fire,
observe: observe,
on: on,
set: set,
teardown: destroy,
_recompute: noop,
_set: _set,
_mount: _mount,
_unmount: _unmount,
_differs: _differs
};
/* generated by Svelte vX.Y.Z */
function link(node) {
function onClick(event) {
event.preventDefault();
history.pushState(null, null, event.target.href);
}
node.addEventListener('click', onClick);
return {
destroy() {
node.removeEventListener('click', onClick);
}
}
}
function create_main_fragment(component, state) {
var a, link_action;
return {
c: function create() {
a = createElement("a");
a.textContent = "Test";
this.h();
},
h: function hydrate() {
a.href = "#";
link_action = link(a) || {};
},
m: function mount(target, anchor) {
insertNode(a, target, anchor);
},
p: noop,
u: function unmount() {
detachNode(a);
},
d: function destroy$$1() {
if (typeof link_action.destroy === 'function') link_action.destroy();
}
};
}
function SvelteComponent(options) {
init(this, options);
this._state = assign({}, options.data);
this._fragment = create_main_fragment(this, this._state);
if (options.target) {
this._fragment.c();
this._mount(options.target, options.anchor);
}
}
assign(SvelteComponent.prototype, proto);
export default SvelteComponent;

@ -0,0 +1,64 @@
/* generated by Svelte vX.Y.Z */
import { assign, createElement, detachNode, init, insertNode, noop, proto } from "svelte/shared.js";
function link(node) {
function onClick(event) {
event.preventDefault();
history.pushState(null, null, event.target.href);
}
node.addEventListener('click', onClick);
return {
destroy() {
node.removeEventListener('click', onClick);
}
}
};
function create_main_fragment(component, state) {
var a, link_action;
return {
c: function create() {
a = createElement("a");
a.textContent = "Test";
this.h();
},
h: function hydrate() {
a.href = "#";
link_action = link(a) || {};
},
m: function mount(target, anchor) {
insertNode(a, target, anchor);
},
p: noop,
u: function unmount() {
detachNode(a);
},
d: function destroy() {
if (typeof link_action.destroy === 'function') link_action.destroy();
}
};
}
function SvelteComponent(options) {
init(this, options);
this._state = assign({}, options.data);
this._fragment = create_main_fragment(this, this._state);
if (options.target) {
this._fragment.c();
this._mount(options.target, options.anchor);
}
}
assign(SvelteComponent.prototype, proto);
export default SvelteComponent;

@ -0,0 +1,23 @@
<a href="#" use:link>Test</a>
<script>
export default {
actions: {
link(node) {
function onClick(event) {
event.preventDefault();
history.pushState(null, null, event.target.href);
}
node.addEventListener('click', onClick);
return {
destroy() {
node.removeEventListener('click', onClick);
}
}
}
}
}
</script>

@ -71,8 +71,7 @@ describe('parse', () => {
});
it('includes AST in svelte.compile output', () => {
const dir = fs.readdirSync('test/parser/samples')[0];
const source = fs.readFileSync(`test/parser/samples/${dir}/input.html`, 'utf-8');
const source = fs.readFileSync(`test/parser/samples/attribute-dynamic/input.html`, 'utf-8');
const { ast } = svelte.compile(source);
const parsed = svelte.parse(source);

@ -0,0 +1 @@
<input use:tooltip="t('tooltip msg')">

@ -0,0 +1,47 @@
{
"hash": 1937205193,
"html": {
"start": 0,
"end": 38,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 38,
"type": "Element",
"name": "input",
"attributes": [
{
"start": 7,
"end": 37,
"type": "Action",
"name": "tooltip",
"expression": {
"type": "CallExpression",
"start": 20,
"end": 36,
"callee": {
"type": "Identifier",
"start": 20,
"end": 21,
"name": "t"
},
"arguments": [
{
"type": "Literal",
"start": 22,
"end": 35,
"value": "tooltip msg",
"raw": "'tooltip msg'"
}
]
}
}
],
"children": []
}
]
},
"css": null,
"js": null
}

@ -0,0 +1,33 @@
{
"hash": 1937205193,
"html": {
"start": 0,
"end": 29,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 29,
"type": "Element",
"name": "input",
"attributes": [
{
"start": 7,
"end": 28,
"type": "Action",
"name": "tooltip",
"expression": {
"type": "Identifier",
"start": 20,
"end": 27,
"name": "message"
}
}
],
"children": []
}
]
},
"css": null,
"js": null
}

@ -0,0 +1 @@
<input use:tooltip="'tooltip msg'">

@ -0,0 +1,34 @@
{
"hash": 1937205193,
"html": {
"start": 0,
"end": 35,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 35,
"type": "Element",
"name": "input",
"attributes": [
{
"start": 7,
"end": 34,
"type": "Action",
"name": "tooltip",
"expression": {
"type": "Literal",
"start": 20,
"end": 33,
"value": "tooltip msg",
"raw": "'tooltip msg'"
}
}
],
"children": []
}
]
},
"css": null,
"js": null
}

@ -0,0 +1 @@
<input use:autofocus>

@ -0,0 +1,28 @@
{
"hash": 1937205193,
"html": {
"start": 0,
"end": 21,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 21,
"type": "Element",
"name": "input",
"attributes": [
{
"start": 7,
"end": 20,
"type": "Action",
"name": "autofocus",
"expression": null
}
],
"children": []
}
]
},
"css": null,
"js": null
}

@ -0,0 +1,22 @@
export default {
html: `
<button>action</button>
`,
test ( assert, component, target, window ) {
const button = target.querySelector( 'button' );
const eventEnter = new window.MouseEvent( 'mouseenter' );
const eventLeave = new window.MouseEvent( 'mouseleave' );
button.dispatchEvent( eventEnter );
assert.htmlEqual( target.innerHTML, `
<button>action</button>
<div class="tooltip">Perform an Action</div>
` );
button.dispatchEvent( eventLeave );
assert.htmlEqual( target.innerHTML, `
<button>action</button>
` );
}
};

@ -0,0 +1,46 @@
<button use:tooltip="t(actionTransKey)">action</button>
<script>
const translations = {
perform_action: 'Perform an Action'
};
function t(key) {
return translations[key] || `{{${key}}}`;
}
export default {
data() {
return { t, actionTransKey: 'perform_action' };
},
actions: {
tooltip(node, text) {
let tooltip = null;
function onMouseEnter() {
tooltip = document.createElement('div');
tooltip.classList.add('tooltip');
tooltip.textContent = text;
node.parentNode.appendChild(tooltip);
}
function onMouseLeave() {
if (!tooltip) return;
tooltip.remove();
tooltip = null;
}
node.addEventListener('mouseenter', onMouseEnter);
node.addEventListener('mouseleave', onMouseLeave);
return {
destroy() {
node.removeEventListener('mouseenter', onMouseEnter);
node.removeEventListener('mouseleave', onMouseLeave);
}
}
}
}
}
</script>

@ -0,0 +1,29 @@
export default {
html: `
<button>action</button>
`,
test ( assert, component, target, window ) {
const button = target.querySelector( 'button' );
const eventEnter = new window.MouseEvent( 'mouseenter' );
const eventLeave = new window.MouseEvent( 'mouseleave' );
const ctrlPress = new window.KeyboardEvent( 'keydown', { ctrlKey: true } );
button.dispatchEvent( eventEnter );
assert.htmlEqual( target.innerHTML, `
<button>action</button>
<div class="tooltip">Perform an Action</div>
` );
window.dispatchEvent( ctrlPress );
assert.htmlEqual( target.innerHTML, `
<button>action</button>
<div class="tooltip">Perform an augmented Action</div>
` );
button.dispatchEvent( eventLeave );
assert.htmlEqual( target.innerHTML, `
<button>action</button>
` );
}
};

@ -0,0 +1,52 @@
<button use:tooltip="tooltip">action</button>
<:Window on:keydown="checkForCtrl(event)" on:keyup="checkForCtrl(event)"/>
<script>
export default {
data() {
return { tooltip: 'Perform an Action' };
},
methods: {
checkForCtrl(event) {
if (event.ctrlKey) {
this.set({ tooltip: 'Perform an augmented Action' });
} else {
this.set({ tooltip: 'Perform an Action' });
}
}
},
actions: {
tooltip(node, text) {
let tooltip = null;
function onMouseEnter() {
tooltip = document.createElement('div');
tooltip.classList.add('tooltip');
tooltip.textContent = text;
node.parentNode.appendChild(tooltip);
}
function onMouseLeave() {
if (!tooltip) return;
tooltip.remove();
tooltip = null;
}
node.addEventListener('mouseenter', onMouseEnter);
node.addEventListener('mouseleave', onMouseLeave);
return {
update(text) {
if (tooltip) tooltip.textContent = text;
},
destroy() {
node.removeEventListener('mouseenter', onMouseEnter);
node.removeEventListener('mouseleave', onMouseLeave);
}
}
}
}
}
</script>

@ -0,0 +1,22 @@
export default {
html: `
<button>action</button>
`,
test ( assert, component, target, window ) {
const button = target.querySelector( 'button' );
const eventEnter = new window.MouseEvent( 'mouseenter' );
const eventLeave = new window.MouseEvent( 'mouseleave' );
button.dispatchEvent( eventEnter );
assert.htmlEqual( target.innerHTML, `
<button>action</button>
<div class="tooltip">Perform an Action</div>
` );
button.dispatchEvent( eventLeave );
assert.htmlEqual( target.innerHTML, `
<button>action</button>
` );
}
};

@ -0,0 +1,34 @@
<button use:tooltip="'Perform an Action'">action</button>
<script>
export default {
actions: {
tooltip(node, text) {
let tooltip = null;
function onMouseEnter() {
tooltip = document.createElement('div');
tooltip.classList.add('tooltip');
tooltip.textContent = text;
node.parentNode.appendChild(tooltip);
}
function onMouseLeave() {
if (!tooltip) return;
tooltip.remove();
tooltip = null;
}
node.addEventListener('mouseenter', onMouseEnter);
node.addEventListener('mouseleave', onMouseLeave);
return {
destroy() {
node.removeEventListener('mouseenter', onMouseEnter);
node.removeEventListener('mouseleave', onMouseLeave);
}
}
}
}
}
</script>

@ -0,0 +1,46 @@
<button use:tooltip="t(actionTransKey)">action</button>
<script>
const translations = {
perform_action: 'Perform an Action'
};
function t(key) {
return translations[key] || `{{${key}}}`;
}
export default {
data() {
return { t, actionTransKey: 'perform_action' };
},
actions: {
tooltip(node, text) {
let tooltip = null;
function onMouseEnter() {
tooltip = document.createElement('div');
tooltip.classList.add('tooltip');
tooltip.textContent = text;
node.parentNode.appendChild(tooltip);
}
function onMouseLeave() {
if (!tooltip) return;
tooltip.remove();
tooltip = null;
}
node.addEventListener('mouseenter', onMouseEnter);
node.addEventListener('mouseleave', onMouseLeave);
return {
destroy() {
node.removeEventListener('mouseenter', onMouseEnter);
node.removeEventListener('mouseleave', onMouseLeave);
}
}
}
}
}
</script>

@ -0,0 +1,12 @@
[{
"message": "Missing action 'whatever'",
"pos": 5,
"loc": {
"line": 1,
"column": 5
},
"end": {
"line": 1,
"column": 17
}
}]

@ -0,0 +1,12 @@
[{
"message": "Actions can only be applied to DOM elements, not components",
"pos": 8,
"loc": {
"line": 1,
"column": 8
},
"end": {
"line": 1,
"column": 15
}
}]

@ -0,0 +1,15 @@
<Widget use:foo/>
<script>
import Widget from './Widget.html';
export default {
components: {
Widget
},
actions: {
foo() {}
}
};
</script>
Loading…
Cancel
Save