Adds actions to components

Actions add additional functionality to elements within your component's template that may be difficult to add with other mechanisms. Examples of functionality which actions makes trivial to attach are:
* tooltips
* image lazy loaders
* drag and drop functionality

Actions can be added to an element with the `use` directive.

```html
<img use:lazyload data-src="giant-photo.jpg>
```

Data may be passed to the action as an object literal (e.g. `use:b="{ setting: true }"`, a literal value (e.g. `use:b="'a string'"`), or a value or function from your component's state (e.g. `add:b="foo"` or `add:b="foo()"`).

Actions are defined in a "actions" property on your component definition.

```html
<script>
  export default {
    actions: {
      b(node, data) {
        // do something
        return {
          update(data) {},
          destroy() {}
        }
      }
    }
  }
</script>
```

A action is a function which receives a reference to an element and optionally the data if it is added in the HTML. This function can then attach listeners or alter the element as needed. The action can optionally return an object with the methods `update(data)` and `destroy()`.

When data is added in the HTML and comes from state, the action's `update(data)` will be called if defined whenever the state is changed.

When the element is removed from the DOM `destroy()` will be called if provided, allowing for cleanup of event listeners, etc.

See https://github.com/sveltejs/svelte/issues/469 for discussion around this feature and more examples of how it could be used.
pull/1247/head
Jacob Wright 7 years ago
parent bc416a538f
commit 04f5d5c975

@ -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