diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index b53851cec9..72edbe8d0e 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -91,6 +91,7 @@ export default class Generator { components: Set; events: Set; transitions: Set; + actions: Set; importedComponents: Map; 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); } diff --git a/src/generators/nodes/Action.ts b/src/generators/nodes/Action.ts new file mode 100644 index 0000000000..fa1320ffac --- /dev/null +++ b/src/generators/nodes/Action.ts @@ -0,0 +1,7 @@ +import Node from './shared/Node'; + +export default class Action extends Node { + name: string; + value: Node[] + expression: Node +} \ No newline at end of file diff --git a/src/generators/nodes/Element.ts b/src/generators/nodes/Element.ts index 42db165f9d..45b6bad6b4 100644 --- a/src/generators/nodes/Element.ts +++ b/src/generators/nodes/Element.ts @@ -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 diff --git a/src/generators/nodes/index.ts b/src/generators/nodes/index.ts index c9602455f3..e3c9de874d 100644 --- a/src/generators/nodes/index.ts +++ b/src/generators/nodes/index.ts @@ -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 = { Attribute, AwaitBlock, + Action, Binding, CatchBlock, Comment, diff --git a/src/parse/read/directives.ts b/src/parse/read/directives.ts index c9c79a85c3..744da4a240 100644 --- a/src/parse/read/directives.ts +++ b/src/parse/read/directives.ts @@ -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\'`' + }, }; diff --git a/src/validate/html/validateElement.ts b/src/validate/html/validateElement.ts index 9172854c7a..3fcec9784b 100644 --- a/src/validate/html/validateElement.ts +++ b/src/validate/html/validateElement.ts @@ -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 + ); + } } }); } diff --git a/src/validate/index.ts b/src/validate/index.ts index 1c68a76306..fd487ccb0f 100644 --- a/src/validate/index.ts +++ b/src/validate/index.ts @@ -33,6 +33,7 @@ export class Validator { methods: Map; helpers: Map; transitions: Map; + actions: Map; slots: Set; used: { @@ -40,6 +41,7 @@ export class Validator { helpers: Set; events: Set; transitions: Set; + actions: Set; }; 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 => { diff --git a/src/validate/js/index.ts b/src/validate/js/index.ts index 927d3bf2f2..a0fc3ee017 100644 --- a/src/validate/js/index.ts +++ b/src/validate/js/index.ts @@ -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); diff --git a/src/validate/js/propValidators/actions.ts b/src/validate/js/propValidators/actions.ts new file mode 100644 index 0000000000..8dc3ad9202 --- /dev/null +++ b/src/validate/js/propValidators/actions.ts @@ -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); +} diff --git a/src/validate/js/propValidators/index.ts b/src/validate/js/propValidators/index.ts index 05819b0837..8641ed36b9 100644 --- a/src/validate/js/propValidators/index.ts +++ b/src/validate/js/propValidators/index.ts @@ -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, diff --git a/test/js/samples/action/expected-bundle.js b/test/js/samples/action/expected-bundle.js new file mode 100644 index 0000000000..fe0b795b7f --- /dev/null +++ b/test/js/samples/action/expected-bundle.js @@ -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; diff --git a/test/js/samples/action/expected.js b/test/js/samples/action/expected.js new file mode 100644 index 0000000000..0eceeae646 --- /dev/null +++ b/test/js/samples/action/expected.js @@ -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; \ No newline at end of file diff --git a/test/js/samples/action/input.html b/test/js/samples/action/input.html new file mode 100644 index 0000000000..5b42061fe5 --- /dev/null +++ b/test/js/samples/action/input.html @@ -0,0 +1,23 @@ +Test + + diff --git a/test/parser/index.js b/test/parser/index.js index 93060ef1ff..7467e31b0c 100644 --- a/test/parser/index.js +++ b/test/parser/index.js @@ -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); diff --git a/test/parser/samples/action-with-call/input.html b/test/parser/samples/action-with-call/input.html new file mode 100644 index 0000000000..246bf02c59 --- /dev/null +++ b/test/parser/samples/action-with-call/input.html @@ -0,0 +1 @@ + diff --git a/test/parser/samples/action-with-call/output.json b/test/parser/samples/action-with-call/output.json new file mode 100644 index 0000000000..f5cc6824f3 --- /dev/null +++ b/test/parser/samples/action-with-call/output.json @@ -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 +} \ No newline at end of file diff --git a/test/parser/samples/action-with-identifier/input.html b/test/parser/samples/action-with-identifier/input.html new file mode 100644 index 0000000000..14a65e83ed --- /dev/null +++ b/test/parser/samples/action-with-identifier/input.html @@ -0,0 +1 @@ + diff --git a/test/parser/samples/action-with-identifier/output.json b/test/parser/samples/action-with-identifier/output.json new file mode 100644 index 0000000000..6c39ed94bc --- /dev/null +++ b/test/parser/samples/action-with-identifier/output.json @@ -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 +} \ No newline at end of file diff --git a/test/parser/samples/action-with-literal/input.html b/test/parser/samples/action-with-literal/input.html new file mode 100644 index 0000000000..60e16eacfc --- /dev/null +++ b/test/parser/samples/action-with-literal/input.html @@ -0,0 +1 @@ + diff --git a/test/parser/samples/action-with-literal/output.json b/test/parser/samples/action-with-literal/output.json new file mode 100644 index 0000000000..0da0318887 --- /dev/null +++ b/test/parser/samples/action-with-literal/output.json @@ -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 +} \ No newline at end of file diff --git a/test/parser/samples/action/input.html b/test/parser/samples/action/input.html new file mode 100644 index 0000000000..64409c2a65 --- /dev/null +++ b/test/parser/samples/action/input.html @@ -0,0 +1 @@ + diff --git a/test/parser/samples/action/output.json b/test/parser/samples/action/output.json new file mode 100644 index 0000000000..597ae297a5 --- /dev/null +++ b/test/parser/samples/action/output.json @@ -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 +} \ No newline at end of file diff --git a/test/runtime/samples/action-function/_config.js b/test/runtime/samples/action-function/_config.js new file mode 100644 index 0000000000..fcbfd2ac82 --- /dev/null +++ b/test/runtime/samples/action-function/_config.js @@ -0,0 +1,22 @@ +export default { + html: ` + + `, + + 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, ` + +
Perform an Action
+ ` ); + + button.dispatchEvent( eventLeave ); + assert.htmlEqual( target.innerHTML, ` + + ` ); + } +}; diff --git a/test/runtime/samples/action-function/main.html b/test/runtime/samples/action-function/main.html new file mode 100644 index 0000000000..cde902caad --- /dev/null +++ b/test/runtime/samples/action-function/main.html @@ -0,0 +1,46 @@ + + + \ No newline at end of file diff --git a/test/runtime/samples/action-update/_config.js b/test/runtime/samples/action-update/_config.js new file mode 100644 index 0000000000..0d472950cb --- /dev/null +++ b/test/runtime/samples/action-update/_config.js @@ -0,0 +1,29 @@ +export default { + html: ` + + `, + + 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, ` + +
Perform an Action
+ ` ); + + window.dispatchEvent( ctrlPress ); + assert.htmlEqual( target.innerHTML, ` + +
Perform an augmented Action
+ ` ); + + button.dispatchEvent( eventLeave ); + assert.htmlEqual( target.innerHTML, ` + + ` ); + } +}; diff --git a/test/runtime/samples/action-update/main.html b/test/runtime/samples/action-update/main.html new file mode 100644 index 0000000000..03bd0d0ef9 --- /dev/null +++ b/test/runtime/samples/action-update/main.html @@ -0,0 +1,52 @@ + +<:Window on:keydown="checkForCtrl(event)" on:keyup="checkForCtrl(event)"/> + + \ No newline at end of file diff --git a/test/runtime/samples/action/_config.js b/test/runtime/samples/action/_config.js new file mode 100644 index 0000000000..fcbfd2ac82 --- /dev/null +++ b/test/runtime/samples/action/_config.js @@ -0,0 +1,22 @@ +export default { + html: ` + + `, + + 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, ` + +
Perform an Action
+ ` ); + + button.dispatchEvent( eventLeave ); + assert.htmlEqual( target.innerHTML, ` + + ` ); + } +}; diff --git a/test/runtime/samples/action/main.html b/test/runtime/samples/action/main.html new file mode 100644 index 0000000000..505d0078b5 --- /dev/null +++ b/test/runtime/samples/action/main.html @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/test/runtime/samples/trait-function/main.html b/test/runtime/samples/trait-function/main.html new file mode 100644 index 0000000000..cde902caad --- /dev/null +++ b/test/runtime/samples/trait-function/main.html @@ -0,0 +1,46 @@ + + + \ No newline at end of file diff --git a/test/validator/samples/action-invalid/errors.json b/test/validator/samples/action-invalid/errors.json new file mode 100644 index 0000000000..1b6c17d9fd --- /dev/null +++ b/test/validator/samples/action-invalid/errors.json @@ -0,0 +1,12 @@ +[{ + "message": "Missing action 'whatever'", + "pos": 5, + "loc": { + "line": 1, + "column": 5 + }, + "end": { + "line": 1, + "column": 17 + } +}] \ No newline at end of file diff --git a/test/validator/samples/action-invalid/input.html b/test/validator/samples/action-invalid/input.html new file mode 100644 index 0000000000..11cc450aaa --- /dev/null +++ b/test/validator/samples/action-invalid/input.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/test/validator/samples/action-on-component/errors.json b/test/validator/samples/action-on-component/errors.json new file mode 100644 index 0000000000..f146d072b0 --- /dev/null +++ b/test/validator/samples/action-on-component/errors.json @@ -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 + } +}] \ No newline at end of file diff --git a/test/validator/samples/action-on-component/input.html b/test/validator/samples/action-on-component/input.html new file mode 100644 index 0000000000..a59dc6ef89 --- /dev/null +++ b/test/validator/samples/action-on-component/input.html @@ -0,0 +1,15 @@ + + + \ No newline at end of file