pull/1367/head
Rich Harris 7 years ago
parent 9ff1beec48
commit 7825c1230a

@ -1,7 +1,18 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
export default class Action extends Node {
type: 'Action';
name: string;
value: Node[]
expression: Node
expression: Expression;
constructor(compiler, parent, info) {
super(compiler, parent, info);
this.name = info.name;
this.expression = info.expression
? new Expression(compiler, this, info.expression)
: null;
}
}

@ -47,7 +47,34 @@ export default class Attribute extends Node {
return expression;
});
this.isDynamic = this.dependencies.size > 0;
this.isDynamic = this.chunks.length === 1
? this.chunks[0].type !== 'Text'
: this.chunks.length > 1;
// TODO this would be better, but it breaks some stuff
// this.isDynamic = this.dependencies.size > 0;
}
getValue() {
if (this.isTrue) return true;
if (this.chunks.length === 0) return `''`;
if (this.chunks.length === 1) {
return this.chunks[0].type === 'Text'
? stringify(this.chunks[0].data)
: this.chunks[0].snippet;
}
return (this.chunks[0].type === 'Text' ? '' : `"" + `) +
this.chunks
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
return getExpressionPrecedence(chunk) <= 13 ? `(${chunk.snippet})` : snippet;
}
})
.join(' + ');
}
render(block: Block) {
@ -287,7 +314,7 @@ export default class Attribute extends Node {
allDependencies.add(d);
});
return getExpressionPrecedence(chunk.expression) <= 13 ? `( ${snippet} )` : snippet;
return getExpressionPrecedence(chunk) <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
@ -295,7 +322,7 @@ export default class Attribute extends Node {
if (allDependencies.size || hasChangeableIndex) {
const dependencies = Array.from(allDependencies);
const condition = (
( block.hasOutroMethod ? `#outroing || ` : '' ) +
(block.hasOutroMethod ? `#outroing || ` : '') +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
);

@ -17,12 +17,36 @@ const readOnlyMediaAttributes = new Set([
export default class Binding extends Node {
name: string;
value: Expression;
obj: string;
prop: string;
constructor(compiler, parent, info) {
super(compiler, parent, info);
this.name = info.name;
this.value = new Expression(compiler, this, info.value);
// const contextual = block.contexts.has(name);
const contextual = false; // TODO
let obj;
let prop;
if (contextual) {
// TODO does this need to go later?
obj = `ctx.${block.listNames.get(name)}`;
prop = `${block.indexNames.get(name)}`;
} else if (this.value.node.type === 'MemberExpression') {
prop = `[✂${this.value.node.property.start}-${this.value.node.property.end}✂]`;
if (!this.value.node.computed) prop = `'${prop}'`;
obj = `[✂${this.value.node.object.start}-${this.value.node.object.end}✂]`;
} else {
obj = 'ctx';
prop = `'${this.name}'`;
}
this.obj = obj;
this.prop = prop;
}
munge(

@ -12,12 +12,17 @@ import Block from '../dom/Block';
import Attribute from './Attribute';
import usesThisOrArguments from '../../validate/js/utils/usesThisOrArguments';
import mapChildren from './shared/mapChildren';
import Binding from './Binding';
import EventHandler from './EventHandler';
export default class Component extends Node {
type: 'Component';
name: string;
attributes: Attribute[];
bindings: Binding[];
handlers: EventHandler[];
children: Node[];
ref: string;
constructor(compiler, parent, info) {
super(compiler, parent, info);
@ -27,7 +32,8 @@ export default class Component extends Node {
this.name = info.name;
this.attributes = [];
// TODO bindings etc
this.bindings = [];
this.handlers = [];
info.attributes.forEach(node => {
switch (node.type) {
@ -36,6 +42,22 @@ export default class Component extends Node {
this.attributes.push(new Attribute(compiler, this, node));
break;
case 'Binding':
this.bindings.push(new Binding(compiler, this, node));
break;
case 'EventHandler':
this.handlers.push(new EventHandler(compiler, this, node));
break;
case 'Ref':
// TODO catch this in validation
if (this.ref) throw new Error(`Duplicate refs`);
compiler.usesRefs = true
this.ref = node.name;
break;
default:
throw new Error(`Not implemented: ${node.type}`);
}
@ -51,23 +73,16 @@ export default class Component extends Node {
) {
this.cannotUseInnerHTML();
this.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Attribute' && attribute.value !== true) {
attribute.value.forEach((chunk: Node) => {
if (chunk.type !== 'Text') {
const dependencies = chunk.metadata.dependencies;
block.addDependencies(dependencies);
}
});
} else {
if (attribute.type === 'EventHandler' && attribute.expression) {
attribute.expression.arguments.forEach((arg: Node) => {
block.addDependencies(arg.metadata.dependencies);
});
} else if (attribute.type === 'Binding' || attribute.type === 'Spread') {
block.addDependencies(attribute.metadata.dependencies);
}
}
this.attributes.forEach(attr => {
block.addDependencies(attr.dependencies);
});
this.bindings.forEach(binding => {
block.addDependencies(binding.value.dependencies);
});
this.handlers.forEach(handler => {
block.addDependencies(handler.dependencies);
});
this.var = block.getUniqueName(
@ -115,60 +130,61 @@ export default class Component extends Node {
let name_updating: string;
let beforecreate: string = null;
const attributes = this.attributes
.filter(a => a.type === 'Attribute' || a.type === 'Spread')
.map(a => mungeAttribute(a, block));
// const attributes = this.attributes
// .filter(a => a.type === 'Attribute' || a.type === 'Spread')
// .map(a => mungeAttribute(a, block));
const bindings = this.attributes
.filter(a => a.type === 'Binding')
.map(a => mungeBinding(a, block));
// const bindings = this.attributes
// .filter(a => a.type === 'Binding')
// .map(a => mungeBinding(a, block));
const eventHandlers = this.attributes
.filter((a: Node) => a.type === 'EventHandler')
.map(a => mungeEventHandler(compiler, this, a, block, allContexts));
const ref = this.attributes.find((a: Node) => a.type === 'Ref');
if (ref) compiler.usesRefs = true;
// const eventHandlers = this.attributes
// .filter((a: Node) => a.type === 'EventHandler')
// .map(a => mungeEventHandler(compiler, this, a, block, allContexts));
const updates: string[] = [];
const usesSpread = !!attributes.find(a => a.spread);
const usesSpread = !!this.attributes.find(a => a.spread);
const attributeObject = usesSpread
? '{}'
: stringifyProps(
attributes.map((attribute: Attribute) => `${attribute.name}: ${attribute.value}`)
// this.attributes.map(attr => `${attr.name}: ${attr.value}`)
this.attributes.map(attr => `${attr.name}: "TODO"`)
);
if (attributes.length || bindings.length) {
if (this.attributes.length || this.bindings.length) {
componentInitProperties.push(`data: ${name_initial_data}`);
}
if ((!usesSpread && attributes.filter(a => a.dynamic).length) || bindings.length) {
if ((!usesSpread && this.attributes.filter(a => a.isDynamic).length) || this.bindings.length) {
updates.push(`var ${name_changes} = {};`);
}
if (attributes.length) {
if (this.attributes.length) {
if (usesSpread) {
const levels = block.getUniqueName(`${this.var}_spread_levels`);
const initialProps = [];
const changes = [];
attributes
.forEach(munged => {
const { spread, name, dynamic, value, dependencies } = munged;
this.attributes
.forEach(attr => {
const { spread, name, dependencies } = attr;
const value = attr.getValue();
const condition = dependencies.size > 0
? [...dependencies].map(d => `changed.${d}`).join(' || ')
: null;
if (spread) {
initialProps.push(value);
const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || ');
changes.push(condition ? `${condition} && ${value}` : value);
} else {
const obj = `{ ${quoteIfNecessary(name)}: ${value} }`;
initialProps.push(obj);
const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || ');
changes.push(condition ? `${condition} && ${obj}` : obj);
}
});
@ -191,27 +207,27 @@ export default class Component extends Node {
]);
`);
} else {
attributes
.filter((attribute: Attribute) => attribute.dynamic)
this.attributes
.filter((attribute: Attribute) => attribute.isDynamic)
.forEach((attribute: Attribute) => {
if (attribute.dependencies.length) {
if (attribute.dependencies.size > 0) {
updates.push(deindent`
if (${attribute.dependencies
if (${[...attribute.dependencies]
.map(dependency => `changed.${dependency}`)
.join(' || ')}) ${name_changes}.${attribute.name} = ${attribute.value};
.join(' || ')}) ${name_changes}.${attribute.name} = ${attribute.getValue()};
`);
}
else {
// TODO this is an odd situation to encounter I *think* it should only happen with
// each block indices, in which case it may be possible to optimise this
updates.push(`${name_changes}.${attribute.name} = ${attribute.value};`);
updates.push(`${name_changes}.${attribute.name} = ${attribute.getValue()};`);
}
});
}
}
if (bindings.length) {
if (this.bindings.length) {
compiler.hasComplexBindings = true;
name_updating = block.alias(`${name}_updating`);
@ -222,18 +238,18 @@ export default class Component extends Node {
const builder = new CodeBuilder();
bindings.forEach((binding: Binding) => {
let { name: key } = getObject(binding.value);
this.bindings.forEach((binding: Binding) => {
let { name: key } = getObject(binding.value.node);
binding.contexts.forEach(context => {
binding.value.contexts.forEach(context => {
allContexts.add(context);
});
let setFromChild;
if (block.contexts.has(key)) {
const computed = isComputed(binding.value);
const tail = binding.value.type === 'MemberExpression' ? getTailSnippet(binding.value) : '';
const computed = isComputed(binding.value.node);
const tail = binding.value.node.type === 'MemberExpression' ? getTailSnippet(binding.value.node) : '';
const list = block.listNames.get(key);
const index = block.indexNames.get(key);
@ -241,7 +257,7 @@ export default class Component extends Node {
setFromChild = deindent`
${list}[${index}]${tail} = childState.${binding.name};
${binding.dependencies
${[...binding.value.dependencies]
.map((name: string) => {
const isStoreProp = name[0] === '$';
const prop = isStoreProp ? name.slice(1) : name;
@ -263,9 +279,9 @@ export default class Component extends Node {
if (isStoreProp) hasStoreBindings = true;
else hasLocalBindings = true;
if (binding.value.type === 'MemberExpression') {
if (binding.value.node.type === 'MemberExpression') {
setFromChild = deindent`
${binding.snippet} = childState.${binding.name};
${binding.value.snippet} = childState.${binding.name};
${newState}.${prop} = state.${key};
`;
}
@ -277,7 +293,7 @@ export default class Component extends Node {
statements.push(deindent`
if (${binding.prop} in ${binding.obj}) {
${name_initial_data}.${binding.name} = ${binding.snippet};
${name_initial_data}.${binding.name} = ${binding.value.snippet};
${name_updating}.${binding.name} = true;
}`
);
@ -288,8 +304,8 @@ export default class Component extends Node {
);
updates.push(deindent`
if (!${name_updating}.${binding.name} && ${binding.dependencies.map((dependency: string) => `changed.${dependency}`).join(' || ')}) {
${name_changes}.${binding.name} = ${binding.snippet};
if (!${name_updating}.${binding.name} && ${[...binding.value.dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')}) {
${name_changes}.${binding.name} = ${binding.value.snippet};
${name_updating}.${binding.name} = true;
}
`);
@ -314,7 +330,7 @@ export default class Component extends Node {
beforecreate = deindent`
#component.root._beforecreate.push(function() {
${name}._bind({ ${bindings.map(b => `${b.name}: 1`).join(', ')} }, ${name}.get());
${name}._bind({ ${this.bindings.map(b => `${b.name}: 1`).join(', ')} }, ${name}.get());
});
`;
}
@ -323,8 +339,7 @@ export default class Component extends Node {
const switch_value = block.getUniqueName('switch_value');
const switch_props = block.getUniqueName('switch_props');
block.contextualise(this.expression);
const { dependencies, snippet } = this.metadata;
const { dependencies, snippet } = this.expression;
const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes);
@ -332,7 +347,7 @@ export default class Component extends Node {
var ${switch_value} = ${snippet};
function ${switch_props}(state) {
${(attributes.length || bindings.length) && deindent`
${(this.attributes.length || this.bindings.length) && deindent`
var ${name_initial_data} = ${attributeObject};`}
${statements}
return {
@ -346,7 +361,7 @@ export default class Component extends Node {
${beforecreate}
}
${eventHandlers.map(handler => deindent`
${this.handlers.map(handler => deindent`
function ${handler.var}(event) {
${handler.body}
}
@ -368,7 +383,7 @@ export default class Component extends Node {
block.builders.mount.addBlock(deindent`
if (${name}) {
${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});
${ref && `#component.refs.${ref.name} = ${name};`}
${this.ref && `#component.refs.${this.ref} = ${name};`}
}
`);
@ -389,12 +404,12 @@ export default class Component extends Node {
${name}.on("${handler.name}", ${handler.var});
`)}
${ref && `#component.refs.${ref.name} = ${name};`}
${this.ref && `#component.refs.${this.ref} = ${name};`}
}
${ref && deindent`
else if (#component.refs.${ref.name} === ${name}) {
#component.refs.${ref.name} = null;
${this.ref && deindent`
else if (#component.refs.${this.ref} === ${name}) {
#component.refs.${this.ref} = null;
}`}
}
`);
@ -418,7 +433,7 @@ export default class Component extends Node {
: `%components-${this.name}`;
block.builders.init.addBlock(deindent`
${(attributes.length || bindings.length) && deindent`
${(this.attributes.length || this.bindings.length) && deindent`
var ${name_initial_data} = ${attributeObject};`}
${statements}
var ${name} = new ${expression}({
@ -427,13 +442,13 @@ export default class Component extends Node {
${beforecreate}
${eventHandlers.map(handler => deindent`
${this.handlers.map(handler => deindent`
${name}.on("${handler.name}", function(event) {
${handler.body}
});
`)}
${ref && `#component.refs.${ref.name} = ${name};`}
${this.ref && `#component.refs.${this.ref} = ${name};`}
`);
block.builders.create.addLine(`${name}._fragment.c();`);
@ -452,7 +467,7 @@ export default class Component extends Node {
block.builders.update.addBlock(deindent`
${updates}
${name}._set(${name_changes});
${bindings.length && `${name_updating} = {};`}
${this.bindings.length && `${name_updating} = {};`}
`);
}
@ -460,7 +475,7 @@ export default class Component extends Node {
block.builders.destroy.addLine(deindent`
${name}.destroy(false);
${ref && `if (#component.refs.${ref.name} === ${name}) #component.refs.${ref.name} = null;`}
${this.ref && `if (#component.refs.${this.ref} === ${name}) #component.refs.${this.ref} = null;`}
`);
}
}
@ -472,8 +487,7 @@ export default class Component extends Node {
function mungeBinding(binding: Node, block: Block): Binding {
const { name } = getObject(binding.value);
const { contexts } = block.contextualise(binding.value);
const { dependencies, snippet } = binding.metadata;
const { dependencies, snippet } = binding.expression;
const contextual = block.contexts.has(name);

@ -23,6 +23,7 @@ export default class Element extends Node {
type: 'Element';
name: string;
attributes: Attribute[];
actions: Action[];
bindings: Binding[];
handlers: EventHandler[];
intro: Transition;
@ -42,6 +43,7 @@ export default class Element extends Node {
parentElement ? parentElement.namespace : this.compiler.namespace;
this.attributes = [];
this.actions = [];
this.bindings = [];
this.handlers = [];
@ -50,6 +52,10 @@ export default class Element extends Node {
info.attributes.forEach(node => {
switch (node.type) {
case 'Action':
this.actions.push(new Action(compiler, this, node));
break;
case 'Attribute':
// special case
if (node.name === 'xmlns') this.namespace = node.value[0].data;
@ -119,6 +125,13 @@ export default class Element extends Node {
}
});
this.actions.forEach(action => {
this.cannotUseInnerHTML();
if (action.expression) {
block.addDependencies(action.expression.dependencies);
}
});
this.bindings.forEach(binding => {
this.cannotUseInnerHTML();
block.addDependencies(binding.value.dependencies);
@ -144,9 +157,7 @@ export default class Element extends Node {
} else {
if (this.parent) this.parent.cannotUseInnerHTML();
if (attribute.type === 'Action' && attribute.expression) {
block.addDependencies(attribute.metadata.dependencies);
} else if (attribute.type === 'Spread') {
if (attribute.type === 'Spread') {
block.addDependencies(attribute.metadata.dependencies);
}
}
@ -739,31 +750,29 @@ export default class Element extends Node {
}
addActions(block: Block) {
this.attributes.filter((a: Action) => a.type === 'Action').forEach((attribute:Action) => {
const { expression } = attribute;
this.actions.forEach(action => {
const { expression } = action;
let snippet, dependencies;
if (expression) {
this.compiler.addSourcemapLocations(expression);
block.contextualise(expression);
snippet = attribute.metadata.snippet;
dependencies = attribute.metadata.dependencies;
snippet = action.expression.snippet;
dependencies = action.expression.dependencies;
}
const name = block.getUniqueName(
`${attribute.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_action`
`${action.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_action`
);
block.addVariable(name);
const fn = `%actions-${attribute.name}`;
const fn = `%actions-${action.name}`;
block.builders.hydrate.addLine(
`${name} = ${fn}.call(#component, ${this.var}${snippet ? `, ${snippet}` : ''}) || {};`
);
if (dependencies && dependencies.length) {
if (dependencies && dependencies.size > 0) {
let conditional = `typeof ${name}.update === 'function' && `;
const deps = dependencies.map(dependency => `changed.${dependency}`).join(' || ');
conditional += dependencies.length > 1 ? `(${deps})` : deps;
const deps = [...dependencies].map(dependency => `changed.${dependency}`).join(' || ');
conditional += dependencies.size > 1 ? `(${deps})` : deps;
block.builders.update.addConditional(
conditional,

@ -71,14 +71,7 @@ export default class Window extends Node {
let usesState = handler.dependencies.size > 0;
// const flattened = flattenReference(handler.expression.callee);
// if (flattened.name !== 'event' && flattened.name !== 'this') {
// // allow event.stopPropagation(), this.select() etc
// compiler.code.prependRight(
// handler.expression.start,
// `${block.alias('component')}.`
// );
// }
handler.render(compiler, block);
const handlerName = block.getUniqueName(`onwindow${handler.name}`);
const handlerBody = deindent`

@ -11,6 +11,8 @@ export default class Expression {
references: Set<string>;
dependencies: Set<string>;
contexts: Set<string>;
indexes: Set<string>;
constructor(compiler, parent, info) {
this.compiler = compiler;
@ -26,6 +28,7 @@ export default class Expression {
const { code, helpers } = compiler;
let { map, scope } = createScopes(info);
const isEventHandler = parent.type === 'EventHandler';
walk(info, {
enter(node: any, parent: any) {
@ -38,11 +41,11 @@ export default class Expression {
}
if (isReference(node, parent)) {
code.prependRight(node.start, 'ctx.');
const { name } = flattenReference(node);
if (scope && scope.has(name) || helpers.has(name) || (name === 'event' && isEventHandler)) return;
code.prependRight(node.start, 'ctx.');
if (contextDependencies.has(name)) {
contextDependencies.get(name).forEach(dependency => {
dependencies.add(dependency);

@ -68,18 +68,17 @@ export default function visitElement(
if (attribute.name === 'value' && node.name === 'textarea') {
textareaContents = stringifyAttributeValue(block, attribute.value);
} else if (attribute.value === true) {
} else if (attribute.isTrue) {
openingTag += ` ${attribute.name}`;
} else if (
booleanAttributes.has(attribute.name) &&
attribute.value.length === 1 &&
attribute.value[0].type !== 'Text'
attribute.chunks.length === 1 &&
attribute.chunks[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
block.contextualise(attribute.value[0].expression);
openingTag += '${' + attribute.value[0].metadata.snippet + ' ? " ' + attribute.name + '" : "" }';
openingTag += '${' + attribute.chunks[0].snippet + ' ? " ' + attribute.name + '" : "" }';
} else {
openingTag += ` ${attribute.name}="${stringifyAttributeValue(block, attribute.value)}"`;
openingTag += ` ${attribute.name}="${stringifyAttributeValue(block, attribute.chunks)}"`;
}
});
}

@ -9,9 +9,7 @@ export default function stringifyAttributeValue(block: Block, chunks: Node[]) {
return escapeTemplate(escape(chunk.data).replace(/"/g, '&quot;'));
}
block.contextualise(chunk.expression);
const { snippet } = chunk.metadata;
return '${__escape(' + snippet + ')}';
return '${__escape(' + chunk.snippet + ')}';
})
.join('');
}

@ -29,12 +29,19 @@ export default function validateHtml(validator: Validator, html: Node) {
validateHead(validator, node, refs, refCallees);
}
else if (node.type === 'Element') {
const isComponent =
node.name === 'svelte:self' ||
node.name === 'svelte:component' ||
validator.components.has(node.name);
else if (node.type === 'Component' || node.name === 'svelte:self' || node.name === 'svelte:component') {
validateElement(
validator,
node,
refs,
refCallees,
stack,
elementStack,
true
);
}
else if (node.type === 'Element') {
validateElement(
validator,
node,
@ -42,12 +49,10 @@ export default function validateHtml(validator: Validator, html: Node) {
refCallees,
stack,
elementStack,
isComponent
false
);
if (!isComponent) {
a11y(validator, node, elementStack);
}
a11y(validator, node, elementStack);
}
else if (node.type === 'EachBlock') {

Loading…
Cancel
Save