merge master -> gh-1257

pull/1299/head
Rich-Harris 8 years ago
commit 304a0e8282

@ -877,6 +877,10 @@ export default class Generator {
if (node.type === 'Component' && node.name === ':Component') {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
}
if (node.type === 'Spread') {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
}
},
leave(node: Node, parent: Node) {

@ -223,10 +223,9 @@ export default class Attribute {
);
}
} else {
const value =
this.value === true
const value = this.value === true
? 'true'
: this.value.length === 0 ? `''` : stringify(this.value[0].data);
: this.value.length === 0 ? `""` : stringify(this.value[0].data);
const statement = (
isLegacyInputType

@ -1,21 +1,14 @@
import deindent from '../../utils/deindent';
import { stringify } from '../../utils/stringify';
import stringifyProps from '../../utils/stringifyProps';
import CodeBuilder from '../../utils/CodeBuilder';
import getTailSnippet from '../../utils/getTailSnippet';
import getObject from '../../utils/getObject';
import getExpressionPrecedence from '../../utils/getExpressionPrecedence';
import isValidIdentifier from '../../utils/isValidIdentifier';
import reservedNames from '../../utils/reservedNames';
import quoteIfNecessary from '../../utils/quoteIfNecessary';
import mungeAttribute from './shared/mungeAttribute';
import Node from './shared/Node';
import Block from '../dom/Block';
import Attribute from './Attribute';
function quoteIfNecessary(name, legacy) {
if (!isValidIdentifier || (legacy && reservedNames.has(name))) return `"${name}"`;
return name;
}
export default class Component extends Node {
type: 'Component';
name: string;
@ -89,12 +82,13 @@ export default class Component extends Node {
const allContexts = new Set();
const statements: string[] = [];
const name_initial_data = block.getUniqueName(`${name}_initial_data`);
const name_changes = block.getUniqueName(`${name}_changes`);
let name_updating: string;
let name_initial_data: string;
let beforecreate: string = null;
const attributes = this.attributes
.filter(a => a.type === 'Attribute')
.filter(a => a.type === 'Attribute' || a.type === 'Spread')
.map(a => mungeAttribute(a, block));
const bindings = this.attributes
@ -110,180 +104,215 @@ export default class Component extends Node {
const updates: string[] = [];
const usesSpread = !!attributes.find(a => a.spread);
const attributeObject = usesSpread
? '{}'
: stringifyProps(
attributes.map((attribute: Attribute) => `${attribute.name}: ${attribute.value}`)
);
if (attributes.length || bindings.length) {
const initialProps = attributes
.map((attribute: Attribute) => `${attribute.name}: ${attribute.value}`);
const initialPropString = stringifyProps(initialProps);
attributes
.filter((attribute: Attribute) => attribute.dynamic)
.forEach((attribute: Attribute) => {
if (attribute.dependencies.length) {
updates.push(deindent`
if (${attribute.dependencies
.map(dependency => `changed.${dependency}`)
.join(' || ')}) ${name}_changes.${attribute.name} = ${attribute.value};
`);
}
componentInitProperties.push(`data: ${name_initial_data}`);
}
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};`);
}
});
if ((!usesSpread && attributes.filter(a => a.dynamic).length) || bindings.length) {
updates.push(`var ${name_changes} = {};`);
}
if (attributes.length) {
if (usesSpread) {
const levels = block.getUniqueName(`${this.var}_spread_levels`);
const initialProps = [];
const changes = [];
if (bindings.length) {
generator.hasComplexBindings = true;
attributes
.forEach(munged => {
const { spread, name, dynamic, value, dependencies } = munged;
name_updating = block.alias(`${name}_updating`);
name_initial_data = block.getUniqueName(`${name}_initial_data`);
if (spread) {
initialProps.push(value);
block.addVariable(name_updating, '{}');
statements.push(`var ${name_initial_data} = ${initialPropString};`);
const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || ');
changes.push(condition ? `${condition} && ${value}` : value);
} else {
const obj = `{ ${quoteIfNecessary(name, this.generator.legacy)}: ${value} }`;
initialProps.push(obj);
let hasLocalBindings = false;
let hasStoreBindings = false;
const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || ');
changes.push(condition ? `${condition} && ${obj}` : obj);
}
});
const builder = new CodeBuilder();
statements.push(deindent`
var ${levels} = [
${initialProps.join(',\n')}
];
bindings.forEach((binding: Binding) => {
let { name: key } = getObject(binding.value);
for (var #i = 0; #i < ${levels}.length; #i += 1) {
${name_initial_data} = @assign(${name_initial_data}, ${levels}[#i]);
}
`);
binding.contexts.forEach(context => {
allContexts.add(context);
updates.push(deindent`
var ${name_changes} = @getSpreadUpdate(${levels}, [
${changes.join(',\n')}
]);
`);
} else {
attributes
.filter((attribute: Attribute) => attribute.dynamic)
.forEach((attribute: Attribute) => {
if (attribute.dependencies.length) {
updates.push(deindent`
if (${attribute.dependencies
.map(dependency => `changed.${dependency}`)
.join(' || ')}) ${name_changes}.${attribute.name} = ${attribute.value};
`);
}
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};`);
}
});
}
}
let setFromChild;
if (bindings.length) {
generator.hasComplexBindings = true;
if (block.contexts.has(key)) {
const computed = isComputed(binding.value);
const tail = binding.value.type === 'MemberExpression' ? getTailSnippet(binding.value) : '';
name_updating = block.alias(`${name}_updating`);
block.addVariable(name_updating, '{}');
const list = block.listNames.get(key);
const index = block.indexNames.get(key);
let hasLocalBindings = false;
let hasStoreBindings = false;
setFromChild = deindent`
${list}[${index}]${tail} = childState.${binding.name};
const builder = new CodeBuilder();
bindings.forEach((binding: Binding) => {
let { name: key } = getObject(binding.value);
binding.contexts.forEach(context => {
allContexts.add(context);
});
let setFromChild;
${binding.dependencies
.map((name: string) => {
const isStoreProp = generator.options.store && name[0] === '$';
const prop = isStoreProp ? name.slice(1) : name;
const newState = isStoreProp ? 'newStoreState' : 'newState';
if (block.contexts.has(key)) {
const computed = isComputed(binding.value);
const tail = binding.value.type === 'MemberExpression' ? getTailSnippet(binding.value) : '';
if (isStoreProp) hasStoreBindings = true;
else hasLocalBindings = true;
const list = block.listNames.get(key);
const index = block.indexNames.get(key);
return `${newState}.${prop} = state.${name};`;
})
.join('\n')}
setFromChild = deindent`
${list}[${index}]${tail} = childState.${binding.name};
${binding.dependencies
.map((name: string) => {
const isStoreProp = generator.options.store && name[0] === '$';
const prop = isStoreProp ? name.slice(1) : name;
const newState = isStoreProp ? 'newStoreState' : 'newState';
if (isStoreProp) hasStoreBindings = true;
else hasLocalBindings = true;
return `${newState}.${prop} = state.${name};`;
})}
`;
}
else {
const isStoreProp = generator.options.store && key[0] === '$';
const prop = isStoreProp ? key.slice(1) : key;
const newState = isStoreProp ? 'newStoreState' : 'newState';
if (isStoreProp) hasStoreBindings = true;
else hasLocalBindings = true;
if (binding.value.type === 'MemberExpression') {
setFromChild = deindent`
${binding.snippet} = childState.${binding.name};
${newState}.${prop} = state.${key};
`;
}
else {
const isStoreProp = generator.options.store && key[0] === '$';
const prop = isStoreProp ? key.slice(1) : key;
const newState = isStoreProp ? 'newStoreState' : 'newState';
if (isStoreProp) hasStoreBindings = true;
else hasLocalBindings = true;
if (binding.value.type === 'MemberExpression') {
setFromChild = deindent`
${binding.snippet} = childState.${binding.name};
${newState}.${prop} = state.${key};
`;
}
else {
setFromChild = `${newState}.${prop} = childState.${binding.name};`;
}
setFromChild = `${newState}.${prop} = childState.${binding.name};`;
}
}
statements.push(deindent`
if (${binding.prop} in ${binding.obj}) {
${name_initial_data}.${binding.name} = ${binding.snippet};
${name_updating}.${binding.name} = true;
}`
);
builder.addConditional(
`!${name_updating}.${binding.name} && changed.${binding.name}`,
setFromChild
);
// TODO could binding.dependencies.length ever be 0?
if (binding.dependencies.length) {
updates.push(deindent`
if (!${name_updating}.${binding.name} && ${binding.dependencies.map((dependency: string) => `changed.${dependency}`).join(' || ')}) {
${name}_changes.${binding.name} = ${binding.snippet};
${name_updating}.${binding.name} = true;
}
`);
}
});
statements.push(deindent`
if (${binding.prop} in ${binding.obj}) {
${name_initial_data}.${binding.name} = ${binding.snippet};
${name_updating}.${binding.name} = true;
}`
);
componentInitProperties.push(`data: ${name_initial_data}`);
const initialisers = [
'state = #component.get()',
hasLocalBindings && 'newState = {}',
hasStoreBindings && 'newStoreState = {}',
].filter(Boolean).join(', ');
componentInitProperties.push(deindent`
_bind: function(changed, childState) {
var ${initialisers};
${builder}
${hasStoreBindings && `#component.store.set(newStoreState);`}
${hasLocalBindings && `#component._set(newState);`}
${name_updating} = {};
builder.addConditional(
`!${name_updating}.${binding.name} && changed.${binding.name}`,
setFromChild
);
updates.push(deindent`
if (!${name_updating}.${binding.name} && ${binding.dependencies.map((dependency: string) => `changed.${dependency}`).join(' || ')}) {
${name_changes}.${binding.name} = ${binding.snippet};
${name_updating}.${binding.name} = true;
}
`);
});
beforecreate = deindent`
#component.root._beforecreate.push(function() {
${name}._bind({ ${bindings.map(b => `${b.name}: 1`).join(', ')} }, ${name}.get());
});
`;
} else if (initialProps.length) {
componentInitProperties.push(`data: ${initialPropString}`);
}
}
const isDynamicComponent = this.name === ':Component';
componentInitProperties.push(`data: ${name_initial_data}`);
const initialisers = [
'state = #component.get()',
hasLocalBindings && 'newState = {}',
hasStoreBindings && 'newStoreState = {}',
].filter(Boolean).join(', ');
componentInitProperties.push(deindent`
_bind: function(changed, childState) {
var ${initialisers};
${builder}
${hasStoreBindings && `#component.store.set(newStoreState);`}
${hasLocalBindings && `#component._set(newState);`}
${name_updating} = {};
}
`);
const switch_vars = isDynamicComponent && {
value: block.getUniqueName('switch_value'),
props: block.getUniqueName('switch_props')
};
beforecreate = deindent`
#component.root._beforecreate.push(function() {
${name}._bind({ ${bindings.map(b => `${b.name}: 1`).join(', ')} }, ${name}.get());
});
`;
}
const expression = (
this.name === ':Self' ? generator.name :
isDynamicComponent ? switch_vars.value :
`%components-${this.name}`
);
if (this.name === ':Component') {
const switch_value = block.getUniqueName('switch_value');
const switch_props = block.getUniqueName('switch_props');
if (isDynamicComponent) {
block.contextualise(this.expression);
const { dependencies, snippet } = this.metadata;
const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes);
block.builders.init.addBlock(deindent`
var ${switch_vars.value} = ${snippet};
var ${switch_value} = ${snippet};
function ${switch_vars.props}(state) {
${statements.length > 0 && statements.join('\n')}
function ${switch_props}(state) {
${(attributes.length || bindings.length) && deindent`
var ${name_initial_data} = ${attributeObject};`}
${statements}
return {
${componentInitProperties.join(',\n')}
};
}
if (${switch_vars.value}) {
var ${name} = new ${expression}(${switch_vars.props}(state));
if (${switch_value}) {
var ${name} = new ${switch_value}(${switch_props}(state));
${beforecreate}
}
@ -317,11 +346,11 @@ export default class Component extends Node {
const updateMountNode = this.getUpdateMountNode(anchor);
block.builders.update.addBlock(deindent`
if (${switch_vars.value} !== (${switch_vars.value} = ${snippet})) {
if (${switch_value} !== (${switch_value} = ${snippet})) {
if (${name}) ${name}.destroy();
if (${switch_vars.value}) {
${name} = new ${switch_vars.value}(${switch_vars.props}(state));
if (${switch_value}) {
${name} = new ${switch_value}(${switch_props}(state));
${name}._fragment.c();
${this.children.map(child => child.remount(name))}
@ -344,9 +373,8 @@ export default class Component extends Node {
if (updates.length) {
block.builders.update.addBlock(deindent`
else {
var ${name}_changes = {};
${updates.join('\n')}
${name}._set(${name}_changes);
${updates}
${name}._set(${name_changes});
${bindings.length && `${name_updating} = {};`}
}
`);
@ -356,8 +384,14 @@ export default class Component extends Node {
block.builders.destroy.addLine(`if (${name}) ${name}.destroy(false);`);
} else {
const expression = this.name === ':Self'
? generator.name
: `%components-${this.name}`;
block.builders.init.addBlock(deindent`
${statements.join('\n')}
${(attributes.length || bindings.length) && deindent`
var ${name_initial_data} = ${attributeObject};`}
${statements}
var ${name} = new ${expression}({
${componentInitProperties.join(',\n')}
});
@ -387,9 +421,9 @@ export default class Component extends Node {
if (updates.length) {
block.builders.update.addBlock(deindent`
var ${name}_changes = {};
${updates.join('\n')}
${name}._set(${name}_changes);
var ${name_changes} = {};
${updates}
${name}._set(${name_changes});
${bindings.length && `${name_updating} = {};`}
`);
}
@ -408,79 +442,6 @@ export default class Component extends Node {
}
}
function mungeAttribute(attribute: Node, block: Block): Attribute {
if (attribute.value === true) {
// attributes without values, e.g. <textarea readonly>
return {
name: attribute.name,
value: true,
dynamic: false
};
}
if (attribute.value.length === 0) {
return {
name: attribute.name,
value: `''`,
dynamic: false
};
}
if (attribute.value.length === 1) {
const value = attribute.value[0];
if (value.type === 'Text') {
// static attributes
return {
name: attribute.name,
value: isNaN(value.data) ? stringify(value.data) : value.data,
dynamic: false
};
}
// simple dynamic attributes
block.contextualise(value.expression); // TODO remove
const { dependencies, snippet } = value.metadata;
// TODO only update attributes that have changed
return {
name: attribute.name,
value: snippet,
dependencies,
dynamic: true
};
}
// otherwise we're dealing with a complex dynamic attribute
const allDependencies = new Set();
const value =
(attribute.value[0].type === 'Text' ? '' : `"" + `) +
attribute.value
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
block.contextualise(chunk.expression); // TODO remove
const { dependencies, snippet } = chunk.metadata;
dependencies.forEach((dependency: string) => {
allDependencies.add(dependency);
});
return getExpressionPrecedence(chunk.expression) <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
return {
name: attribute.name,
value,
dependencies: Array.from(allDependencies),
dynamic: true
};
}
function mungeBinding(binding: Node, block: Block): Binding {
const { name } = getObject(binding.value);
const { contexts } = block.contextualise(binding.value);
@ -555,4 +516,4 @@ function isComputed(node: Node) {
}
return false;
}
}

@ -5,6 +5,8 @@ import isVoidElementName from '../../utils/isVoidElementName';
import validCalleeObjects from '../../utils/validCalleeObjects';
import reservedNames from '../../utils/reservedNames';
import fixAttributeCasing from '../../utils/fixAttributeCasing';
import quoteIfNecessary from '../../utils/quoteIfNecessary';
import mungeAttribute from './shared/mungeAttribute';
import Node from './shared/Node';
import Block from '../dom/Block';
import Attribute from './Attribute';
@ -141,6 +143,10 @@ export default class Element extends Node {
component._slots.add(slot);
}
if (this.spread) {
block.addDependencies(this.spread.metadata.dependencies);
}
this.var = block.getUniqueName(
this.name.replace(/[^a-zA-Z0-9_$]/g, '_')
);
@ -238,9 +244,9 @@ export default class Element extends Node {
}
this.addBindings(block, allUsedContexts);
this.addAttributes(block);
const eventHandlerUsesComponent = this.addEventHandlers(block, allUsedContexts);
this.addRefs(block);
this.addAttributes(block);
this.addTransitions(block);
this.addActions(block);
@ -432,11 +438,69 @@ export default class Element extends Node {
}
addAttributes(block: Block) {
if (this.attributes.find(attr => attr.type === 'Spread')) {
this.addSpreadAttributes(block);
return;
}
this.attributes.filter((a: Attribute) => a.type === 'Attribute').forEach((attribute: Attribute) => {
attribute.render(block);
});
}
addSpreadAttributes(block: Block) {
const levels = block.getUniqueName(`${this.var}_levels`);
const data = block.getUniqueName(`${this.var}_data`);
const initialProps = [];
const updates = [];
this.attributes
.filter(attr => attr.type === 'Attribute' || attr.type === 'Spread')
.forEach(attr => {
if (attr.type === 'Attribute') {
const { dynamic, value, dependencies } = mungeAttribute(attr, block);
const snippet = `{ ${quoteIfNecessary(attr.name, this.generator.legacy)}: ${value} }`;
initialProps.push(snippet);
const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || ');
updates.push(condition ? `${condition} && ${snippet}` : snippet);
}
else {
block.contextualise(attr.expression); // TODO gah
const { snippet, dependencies } = attr.metadata;
initialProps.push(snippet);
const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || ');
updates.push(condition ? `${condition} && ${snippet}` : snippet);
}
});
block.builders.init.addBlock(deindent`
var ${levels} = [
${initialProps.join(',\n')}
];
var ${data} = {};
for (var #i = 0; #i < ${levels}.length; #i += 1) {
${data} = @assign(${data}, ${levels}[#i]);
}
`);
block.builders.hydrate.addLine(
`@setAttributes(${this.var}, ${data});`
);
block.builders.update.addBlock(deindent`
@setAttributes(${this.var}, @getSpreadUpdate(${levels}, [
${updates.join(',\n')}
]));
`);
}
addEventHandlers(block: Block, allUsedContexts) {
const { generator } = this;
let eventHandlerUsesComponent = false;
@ -553,6 +617,7 @@ export default class Element extends Node {
}
addRefs(block: Block) {
// TODO it should surely be an error to have more than one ref
this.attributes.filter((a: Ref) => a.type === 'Ref').forEach((attribute: Ref) => {
const ref = `#component.refs.${attribute.name}`;

@ -51,4 +51,4 @@ const nodes: Record<string, any> = {
Window
};
export default nodes;
export default nodes;

@ -0,0 +1,107 @@
import { stringify } from '../../../utils/stringify';
import getExpressionPrecedence from '../../../utils/getExpressionPrecedence';
import Node from './Node';
import Attribute from '../Attribute';
import Block from '../../dom/Block';
type MungedAttribute = {
spread: boolean;
name: string;
value: string | true;
dependencies: string[];
dynamic: boolean;
}
export default function mungeAttribute(attribute: Node, block: Block): MungedAttribute {
if (attribute.type === 'Spread') {
block.contextualise(attribute.expression); // TODO remove
const { dependencies, snippet } = attribute.metadata;
return {
spread: true,
name: null,
value: snippet,
dynamic: dependencies.length > 0,
dependencies
};
}
if (attribute.value === true) {
// attributes without values, e.g. <textarea readonly>
return {
spread: false,
name: attribute.name,
value: true,
dynamic: false,
dependencies: []
};
}
if (attribute.value.length === 0) {
return {
spread: false,
name: attribute.name,
value: `''`,
dynamic: false,
dependencies: []
};
}
if (attribute.value.length === 1) {
const value = attribute.value[0];
if (value.type === 'Text') {
// static attributes
return {
spread: false,
name: attribute.name,
value: isNaN(value.data) ? stringify(value.data) : value.data,
dynamic: false,
dependencies: []
};
}
// simple dynamic attributes
block.contextualise(value.expression); // TODO remove
const { dependencies, snippet } = value.metadata;
// TODO only update attributes that have changed
return {
spread: false,
name: attribute.name,
value: snippet,
dependencies,
dynamic: true
};
}
// otherwise we're dealing with a complex dynamic attribute
const allDependencies = new Set();
const value =
(attribute.value[0].type === 'Text' ? '' : `"" + `) +
attribute.value
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
block.contextualise(chunk.expression); // TODO remove
const { dependencies, snippet } = chunk.metadata;
dependencies.forEach((dependency: string) => {
allDependencies.add(dependency);
});
return getExpressionPrecedence(chunk.expression) <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
return {
spread: false,
name: attribute.name,
value,
dependencies: Array.from(allDependencies),
dynamic: true
};
}

@ -239,6 +239,24 @@ export default function ssr(
};
`
}
${
/__spread/.test(generator.renderCode) && deindent`
function __spread(args) {
const attributes = Object.assign({}, ...args);
let str = '';
Object.keys(attributes).forEach(name => {
const value = attributes[name];
if (value === undefined) return;
if (value === true) str += " " + name;
str += " " + name + "=" + JSON.stringify(value);
});
return str;
}
`
}
`.replace(/(@+|#+|%+)(\w*(?:-\w*)?)/g, (match: string, sigil: string, name: string) => {
if (sigil === '@') return generator.alias(name);
if (sigil === '%') return generator.templateVars.get(name);

@ -27,51 +27,65 @@ export default function visitComponent(
const attributes: Node[] = [];
const bindings: Node[] = [];
let usesSpread;
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Attribute') {
if (attribute.type === 'Attribute' || attribute.type === 'Spread') {
if (attribute.type === 'Spread') usesSpread = true;
attributes.push(attribute);
} else if (attribute.type === 'Binding') {
bindings.push(attribute);
}
});
const props = attributes
.map(attribute => {
let value;
if (attribute.value === true) {
value = `true`;
} else if (attribute.value.length === 0) {
value = `''`;
} else if (attribute.value.length === 1) {
const chunk = attribute.value[0];
if (chunk.type === 'Text') {
value = isNaN(chunk.data) ? stringify(chunk.data) : chunk.data;
} else {
block.contextualise(chunk.expression);
const { snippet } = chunk.metadata;
value = snippet;
}
} else {
value = '`' + attribute.value.map(stringifyAttribute).join('') + '`';
const bindingProps = bindings.map(binding => {
const { name } = getObject(binding.value);
const tail = binding.value.type === 'MemberExpression'
? getTailSnippet(binding.value)
: '';
const keypath = block.contexts.has(name)
? `${name}${tail}`
: `state.${name}${tail}`;
return `${binding.name}: ${keypath}`;
});
function getAttributeValue(attribute) {
if (attribute.value === true) return `true`;
if (attribute.value.length === 0) return `''`;
if (attribute.value.length === 1) {
const chunk = attribute.value[0];
if (chunk.type === 'Text') {
return isNaN(chunk.data) ? stringify(chunk.data) : chunk.data;
}
return `${attribute.name}: ${value}`;
})
.concat(
bindings.map(binding => {
const { name } = getObject(binding.value);
const tail = binding.value.type === 'MemberExpression'
? getTailSnippet(binding.value)
: '';
const keypath = block.contexts.has(name)
? `${name}${tail}`
: `state.${name}${tail}`;
return `${binding.name}: ${keypath}`;
})
)
.join(', ');
block.contextualise(chunk.expression);
const { snippet } = chunk.metadata;
return snippet;
}
return '`' + attribute.value.map(stringifyAttribute).join('') + '`';
}
const props = usesSpread
? `Object.assign(${
attributes
.map(attribute => {
if (attribute.type === 'Spread') {
block.contextualise(attribute.expression);
return attribute.metadata.snippet;
} else {
return `{ ${attribute.name}: ${getAttributeValue(attribute)} }`;
}
})
.concat(bindingProps.map(p => `{ ${p} }`))
.join(', ')
})`
: `{ ${attributes
.map(attribute => `${attribute.name}: ${getAttributeValue(attribute)}`)
.concat(bindingProps)
.join(', ')} }`;
const isDynamicComponent = node.name === ':Component';
if (isDynamicComponent) block.contextualise(node.expression);
@ -86,7 +100,7 @@ export default function visitComponent(
block.addBinding(binding, expression);
});
let open = `\${${expression}._render(__result, {${props}}`;
let open = `\${${expression}._render(__result, ${props}`;
const options = [];
if (generator.options.store) {

@ -1,6 +1,7 @@
import visitComponent from './Component';
import visitSlot from './Slot';
import isVoidElementName from '../../../utils/isVoidElementName';
import quoteIfNecessary from '../../../utils/quoteIfNecessary';
import visit from '../visit';
import { SsrGenerator } from '../index';
import Element from '../../nodes/Element';
@ -34,25 +35,54 @@ export default function visitElement(
appendTarget.slots[slotName] = '';
}
node.attributes.forEach((attribute: Node) => {
if (attribute.type !== 'Attribute') return;
if (node.attributes.find(attr => attr.type === 'Spread')) {
// TODO dry this out
const args = [];
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Spread') {
block.contextualise(attribute.expression);
args.push(attribute.metadata.snippet);
} else if (attribute.type === 'Attribute') {
if (attribute.name === 'value' && node.name === 'textarea') {
textareaContents = stringifyAttributeValue(block, attribute.value);
} else if (attribute.value === true) {
args.push(`{ ${quoteIfNecessary(attribute.name)}: true }`);
} else if (
booleanAttributes.has(attribute.name) &&
attribute.value.length === 1 &&
attribute.value[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
block.contextualise(attribute.value[0].expression);
args.push(`{ ${quoteIfNecessary(attribute.name)}: ${attribute.value[0].metadata.snippet} }`);
} else {
args.push(`{ ${quoteIfNecessary(attribute.name)}: \`${stringifyAttributeValue(block, attribute.value)}\` }`);
}
}
});
openingTag += "${__spread([" + args.join(', ') + "])}";
} else {
node.attributes.forEach((attribute: Node) => {
if (attribute.type !== 'Attribute') return;
if (attribute.name === 'value' && node.name === 'textarea') {
textareaContents = stringifyAttributeValue(block, attribute.value);
} else if (attribute.value === true) {
openingTag += ` ${attribute.name}`;
} else if (
booleanAttributes.has(attribute.name) &&
attribute.value.length === 1 &&
attribute.value[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 + '" : "" }';
} else {
openingTag += ` ${attribute.name}="${stringifyAttributeValue(block, attribute.value)}"`;
}
});
if (attribute.name === 'value' && node.name === 'textarea') {
textareaContents = stringifyAttributeValue(block, attribute.value);
} else if (attribute.value === true) {
openingTag += ` ${attribute.name}`;
} else if (
booleanAttributes.has(attribute.name) &&
attribute.value.length === 1 &&
attribute.value[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 + '" : "" }';
} else {
openingTag += ` ${attribute.name}="${stringifyAttributeValue(block, attribute.value)}"`;
}
});
}
if (node._cssRefAttribute) {
openingTag += ` svelte-ref-${node._cssRefAttribute}`;

@ -8,19 +8,7 @@ import reservedNames from '../utils/reservedNames';
import fullCharCodeAt from '../utils/fullCharCodeAt';
import hash from '../utils/hash';
import { Node, Parsed } from '../interfaces';
import CompileError from '../utils/CompileError';
class ParseError extends CompileError {
constructor(
message: string,
template: string,
index: number,
filename: string
) {
super(message, template, index, filename);
this.name = 'ParseError';
}
}
import error from '../utils/error';
interface ParserOptions {
filename?: string;
@ -109,7 +97,12 @@ export class Parser {
}
error(message: string, index = this.index) {
throw new ParseError(message, this.template, index, this.filename);
error(message, {
name: 'ParseError',
source: this.template,
start: index,
filename: this.filename
});
}
eat(str: string, required?: boolean, message?: string) {

@ -289,6 +289,23 @@ function readTagName(parser: Parser) {
function readAttribute(parser: Parser, uniqueNames: Set<string>) {
const start = parser.index;
if (parser.eat('{{')) {
parser.allowWhitespace();
parser.eat('...', true, 'Expected spread operator (...)');
const expression = readExpression(parser);
parser.allowWhitespace();
parser.eat('}}', true);
return {
start,
end: parser.index,
type: 'Spread',
expression
};
}
let name = parser.readUntil(/(\s|=|\/|>)/);
if (!name) return null;
if (uniqueNames.has(name)) {
@ -383,4 +400,4 @@ function readSequence(parser: Parser, done: () => boolean) {
}
parser.error(`Unexpected end of input`);
}
}

@ -85,6 +85,21 @@ export function setAttribute(node, attribute, value) {
node.setAttribute(attribute, value);
}
export function setAttributes(node, attributes) {
for (var key in attributes) {
if (key in node) {
node[key] = attributes[key];
} else {
if (attributes[key] === undefined) removeAttribute(node, key);
else setAttribute(node, key, attributes[key]);
}
}
}
export function removeAttribute(node, attribute) {
node.removeAttribute(attribute);
}
export function setXlinkAttribute(node, attribute, value) {
node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);
}
@ -177,4 +192,4 @@ export function selectMultipleValue(select) {
return [].map.call(select.querySelectorAll(':checked'), function(option) {
return option.__value;
});
}
}

@ -2,6 +2,7 @@ import { assign } from './utils.js';
import { noop } from './utils.js';
export * from './dom.js';
export * from './keyed-each.js';
export * from './spread.js';
export * from './transitions.js';
export * from './utils.js';

@ -0,0 +1,37 @@
export function getSpreadUpdate(levels, updates) {
var update = {};
var to_null_out = {};
var accounted_for = {};
var i = levels.length;
while (i--) {
var o = levels[i];
var n = updates[i];
if (n) {
for (var key in o) {
if (!(key in n)) to_null_out[key] = 1;
}
for (var key in n) {
if (!accounted_for[key]) {
update[key] = n[key];
accounted_for[key] = 1;
}
}
levels[i] = n;
} else {
for (var key in o) {
accounted_for[key] = 1;
}
}
}
for (var key in to_null_out) {
if (!(key in update)) update[key] = undefined;
}
return update;
}

@ -1,35 +0,0 @@
import { locate } from 'locate-character';
import getCodeFrame from '../utils/getCodeFrame';
export default class CompileError extends Error {
frame: string;
loc: { line: number; column: number };
end: { line: number; column: number };
pos: number;
filename: string;
constructor(
message: string,
template: string,
startPos: number,
filename: string,
endPos: number = startPos
) {
super(message);
const start = locate(template, startPos);
const end = locate(template, endPos);
this.loc = { line: start.line + 1, column: start.column };
this.end = { line: end.line + 1, column: end.column };
this.pos = startPos;
this.filename = filename;
this.frame = getCodeFrame(template, start.line, start.column);
}
public toString = () => {
return `${this.message} (${this.loc.line}:${this.loc.column})\n${this
.frame}`;
}
}

@ -0,0 +1,37 @@
import { locate } from 'locate-character';
import getCodeFrame from '../utils/getCodeFrame';
class CompileError extends Error {
loc: { line: number, column: number };
end: { line: number, column: number };
pos: number;
filename: string;
frame: string;
toString() {
return `${this.message} (${this.loc.line}:${this.loc.column})\n${this.frame}`;
}
}
export default function error(message: string, props: {
name: string,
source: string,
filename: string,
start: number,
end?: number
}) {
const error = new CompileError(message);
error.name = props.name;
const start = locate(props.source, props.start);
const end = locate(props.source, props.end || props.start);
error.loc = { line: start.line + 1, column: start.column };
error.end = { line: end.line + 1, column: end.column };
error.pos = props.start;
error.filename = props.filename;
error.frame = getCodeFrame(props.source, start.line, start.column);
throw error;
}

@ -0,0 +1,7 @@
import isValidIdentifier from './isValidIdentifier';
import reservedNames from './reservedNames';
export default function quoteIfNecessary(name: string, legacy?: boolean) {
if (!isValidIdentifier(name) || (legacy && reservedNames.has(name))) return `"${name}"`;
return name;
}

@ -27,6 +27,8 @@ export default function a11y(
const attributeMap = new Map();
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Spread') return;
const name = attribute.name.toLowerCase();
// aria-props

@ -2,23 +2,11 @@ import validateJs from './js/index';
import validateHtml from './html/index';
import { getLocator, Location } from 'locate-character';
import getCodeFrame from '../utils/getCodeFrame';
import CompileError from '../utils/CompileError';
import Stats from '../Stats';
import error from '../utils/error';
import Stylesheet from '../css/Stylesheet';
import { Node, Parsed, CompileOptions, Warning } from '../interfaces';
class ValidationError extends CompileError {
constructor(
message: string,
template: string,
pos: { start: number, end: number },
filename: string,
) {
super(message, template, pos.start, filename, pos.end);
this.name = 'ValidationError';
}
}
export class Validator {
readonly source: string;
readonly filename: string;
@ -72,7 +60,13 @@ export class Validator {
}
error(message: string, pos: { start: number, end: number }) {
throw new ValidationError(message, this.source, pos, this.filename);
error(message, {
name: 'ValidationError',
source: this.source,
start: pos.start,
end: pos.end,
filename: this.filename
});
}
warn(message: string, pos: { start: number, end: number }) {

@ -176,7 +176,7 @@ var proto = {
/* generated by Svelte vX.Y.Z */
function link(node) {
function onClick(event) {
event.preventDefault();
history.pushState(null, null, event.target.href);

@ -2,7 +2,7 @@
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);

@ -171,9 +171,10 @@ var Nested = window.Nested;
function create_main_fragment(component, state) {
var nested_initial_data = { foo: "bar" };
var nested = new Nested({
root: component.root,
data: { foo: "bar" }
data: nested_initial_data
});
return {

@ -5,9 +5,10 @@ var Nested = window.Nested;
function create_main_fragment(component, state) {
var nested_initial_data = { foo: "bar" };
var nested = new Nested({
root: component.root,
data: { foo: "bar" }
data: nested_initial_data
});
return {

@ -171,9 +171,10 @@ var Nested = window.Nested;
function create_main_fragment(component, state) {
var nested_initial_data = { foo: "bar" };
var nested = new Nested({
root: component.root,
data: { foo: "bar" }
data: nested_initial_data
});
return {

@ -5,9 +5,10 @@ var Nested = window.Nested;
function create_main_fragment(component, state) {
var nested_initial_data = { foo: "bar" };
var nested = new Nested({
root: component.root,
data: { foo: "bar" }
data: nested_initial_data
});
return {

@ -167,9 +167,10 @@ var Nested = window.Nested;
function create_main_fragment(component, state) {
var nested_initial_data = { foo: "bar" };
var nested = new Nested({
root: component.root,
data: { foo: "bar" }
data: nested_initial_data
});
return {

@ -5,9 +5,10 @@ var Nested = window.Nested;
function create_main_fragment(component, state) {
var nested_initial_data = { foo: "bar" };
var nested = new Nested({
root: component.root,
data: { foo: "bar" }
data: nested_initial_data
});
return {

@ -61,4 +61,4 @@ assign(assign(SvelteComponent.prototype, proto), {
this.parentNode.removeChild(this);
}
});
export default SvelteComponent;
export default SvelteComponent;

@ -121,4 +121,4 @@ function SvelteComponent(options) {
}
assign(SvelteComponent.prototype, proto);
export default SvelteComponent;
export default SvelteComponent;

@ -166,4 +166,4 @@ function SvelteComponent(options) {
}
assign(SvelteComponent.prototype, proto);
export default SvelteComponent;
export default SvelteComponent;

@ -57,4 +57,4 @@ function SvelteComponent(options) {
}
assign(assign(SvelteComponent.prototype, methods), proto);
export default SvelteComponent;
export default SvelteComponent;

@ -47,4 +47,4 @@ function SvelteComponent(options) {
assign(assign(SvelteComponent.prototype, methods), proto);
setup(SvelteComponent);
export default SvelteComponent;
export default SvelteComponent;

@ -59,4 +59,4 @@ SvelteComponent.renderCss = function() {
};
};
module.exports = SvelteComponent;
module.exports = SvelteComponent;

@ -0,0 +1 @@
<div {{...props}}></div>

@ -0,0 +1,32 @@
{
"hash": "phg0l6",
"html": {
"start": 0,
"end": 24,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 24,
"type": "Element",
"name": "div",
"attributes": [
{
"start": 5,
"end": 17,
"type": "Spread",
"expression": {
"type": "Identifier",
"start": 10,
"end": 15,
"name": "props"
}
}
],
"children": []
}
]
},
"css": null,
"js": null
}

@ -0,0 +1,4 @@
<p>foo: {{foo}}</p>
<p>baz: {{baz}} ({{typeof baz}})</p>
<p>qux: {{qux}}</p>
<p>quux: {{quux}}</p>

@ -0,0 +1,25 @@
export default {
data: {
props: {
foo: 'lol',
baz: 40 + 2,
qux: `this is a ${'piece of'} string`,
quux: 'core'
}
},
html: `<div><p>foo: lol</p>\n<p>baz: 42 (number)</p>\n<p>qux: named</p>\n<p>quux: core</p></div>`,
test ( assert, component, target ) {
component.set({
props: {
foo: 'wut',
baz: 40 + 3,
qux: `this is a ${'rather boring'} string`,
quux: 'heart'
}
});
assert.equal( target.innerHTML, `<div><p>foo: wut</p>\n<p>baz: 43 (number)</p>\n<p>qux: named</p>\n<p>quux: heart</p></div>` );
}
};

@ -0,0 +1,11 @@
<div>
<Widget {{...props}} qux="named"/>
</div>
<script>
import Widget from './Widget.html';
export default {
components: { Widget }
};
</script>

@ -0,0 +1,27 @@
export default {
data: {
props: {
disabled: true
}
},
html: `
<button disabled>click me</button>
`,
test(assert, component, target) {
const button = target.querySelector('button');
assert.ok(button.disabled);
component.set({
props: { disabled: false }
});
assert.htmlEqual(
target.innerHTML,
`<button>click me</button>`
);
assert.ok(!button.disabled);
},
};

@ -0,0 +1 @@
<button {{...props}} >click me</button>

@ -0,0 +1,33 @@
export default {
data: {
a: {
'data-one': 1,
'data-two': 2,
},
c: {
'data-b': 'overridden',
},
d: 'deeeeee',
},
html: `
<div data-one="1" data-two="2" data-b="overridden" data-d="deeeeee" >test</div>
`,
test(assert, component, target) {
component.set({
a: {
'data-one': 10
},
c: {
'data-c': 'new'
},
d: 'DEEEEEE'
});
assert.htmlEqual(
target.innerHTML,
`<div data-one="10" data-b="b" data-c="new" data-d="DEEEEEE" >test</div>`
);
},
};

@ -0,0 +1 @@
<div {{...a}} data-b="b" {{...c}} data-d={{d}} >test</div>

@ -0,0 +1,19 @@
export default {
html: `<div data-named="value" data-foo="bar">red</div>`,
test ( assert, component, target ) {
const div = target.querySelector( 'div' );
assert.equal( div.dataset.foo, 'bar' );
assert.equal( div.dataset.named, 'value' );
component.set({ color: 'blue', props: { 'data-foo': 'baz', 'data-named': 'qux' } });
assert.htmlEqual( target.innerHTML, `<div data-named="value" data-foo="baz">blue</div>` );
assert.equal( div.dataset.foo, 'baz' );
assert.equal( div.dataset.named, 'value' );
component.set({ color: 'blue', props: {} });
assert.htmlEqual( target.innerHTML, `<div data-named="value">blue</div>` );
assert.equal( div.dataset.foo, undefined );
}
};

@ -0,0 +1,13 @@
<div {{...props}} data-named="value">{{color}}</div>
<script>
export default {
data: () => ({
color: 'red',
props: {
'data-foo': 'bar',
'data-named': 'qux'
}
})
};
</script>
Loading…
Cancel
Save