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') { if (node.type === 'Component' && node.name === ':Component') {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false); 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) { leave(node: Node, parent: Node) {

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

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

@ -5,6 +5,8 @@ import isVoidElementName from '../../utils/isVoidElementName';
import validCalleeObjects from '../../utils/validCalleeObjects'; import validCalleeObjects from '../../utils/validCalleeObjects';
import reservedNames from '../../utils/reservedNames'; import reservedNames from '../../utils/reservedNames';
import fixAttributeCasing from '../../utils/fixAttributeCasing'; import fixAttributeCasing from '../../utils/fixAttributeCasing';
import quoteIfNecessary from '../../utils/quoteIfNecessary';
import mungeAttribute from './shared/mungeAttribute';
import Node from './shared/Node'; import Node from './shared/Node';
import Block from '../dom/Block'; import Block from '../dom/Block';
import Attribute from './Attribute'; import Attribute from './Attribute';
@ -141,6 +143,10 @@ export default class Element extends Node {
component._slots.add(slot); component._slots.add(slot);
} }
if (this.spread) {
block.addDependencies(this.spread.metadata.dependencies);
}
this.var = block.getUniqueName( this.var = block.getUniqueName(
this.name.replace(/[^a-zA-Z0-9_$]/g, '_') this.name.replace(/[^a-zA-Z0-9_$]/g, '_')
); );
@ -238,9 +244,9 @@ export default class Element extends Node {
} }
this.addBindings(block, allUsedContexts); this.addBindings(block, allUsedContexts);
this.addAttributes(block);
const eventHandlerUsesComponent = this.addEventHandlers(block, allUsedContexts); const eventHandlerUsesComponent = this.addEventHandlers(block, allUsedContexts);
this.addRefs(block); this.addRefs(block);
this.addAttributes(block);
this.addTransitions(block); this.addTransitions(block);
this.addActions(block); this.addActions(block);
@ -432,11 +438,69 @@ export default class Element extends Node {
} }
addAttributes(block: Block) { 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) => { this.attributes.filter((a: Attribute) => a.type === 'Attribute').forEach((attribute: Attribute) => {
attribute.render(block); 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) { addEventHandlers(block: Block, allUsedContexts) {
const { generator } = this; const { generator } = this;
let eventHandlerUsesComponent = false; let eventHandlerUsesComponent = false;
@ -553,6 +617,7 @@ export default class Element extends Node {
} }
addRefs(block: Block) { 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) => { this.attributes.filter((a: Ref) => a.type === 'Ref').forEach((attribute: Ref) => {
const ref = `#component.refs.${attribute.name}`; const ref = `#component.refs.${attribute.name}`;

@ -51,4 +51,4 @@ const nodes: Record<string, any> = {
Window 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) => { `.replace(/(@+|#+|%+)(\w*(?:-\w*)?)/g, (match: string, sigil: string, name: string) => {
if (sigil === '@') return generator.alias(name); if (sigil === '@') return generator.alias(name);
if (sigil === '%') return generator.templateVars.get(name); if (sigil === '%') return generator.templateVars.get(name);

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

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

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

@ -289,6 +289,23 @@ function readTagName(parser: Parser) {
function readAttribute(parser: Parser, uniqueNames: Set<string>) { function readAttribute(parser: Parser, uniqueNames: Set<string>) {
const start = parser.index; 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|=|\/|>)/); let name = parser.readUntil(/(\s|=|\/|>)/);
if (!name) return null; if (!name) return null;
if (uniqueNames.has(name)) { if (uniqueNames.has(name)) {
@ -383,4 +400,4 @@ function readSequence(parser: Parser, done: () => boolean) {
} }
parser.error(`Unexpected end of input`); parser.error(`Unexpected end of input`);
} }

@ -85,6 +85,21 @@ export function setAttribute(node, attribute, value) {
node.setAttribute(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) { export function setXlinkAttribute(node, attribute, value) {
node.setAttributeNS('http://www.w3.org/1999/xlink', 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 [].map.call(select.querySelectorAll(':checked'), function(option) {
return option.__value; return option.__value;
}); });
} }

@ -2,6 +2,7 @@ import { assign } from './utils.js';
import { noop } from './utils.js'; import { noop } from './utils.js';
export * from './dom.js'; export * from './dom.js';
export * from './keyed-each.js'; export * from './keyed-each.js';
export * from './spread.js';
export * from './transitions.js'; export * from './transitions.js';
export * from './utils.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(); const attributeMap = new Map();
node.attributes.forEach((attribute: Node) => { node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Spread') return;
const name = attribute.name.toLowerCase(); const name = attribute.name.toLowerCase();
// aria-props // aria-props

@ -2,23 +2,11 @@ import validateJs from './js/index';
import validateHtml from './html/index'; import validateHtml from './html/index';
import { getLocator, Location } from 'locate-character'; import { getLocator, Location } from 'locate-character';
import getCodeFrame from '../utils/getCodeFrame'; import getCodeFrame from '../utils/getCodeFrame';
import CompileError from '../utils/CompileError';
import Stats from '../Stats'; import Stats from '../Stats';
import error from '../utils/error';
import Stylesheet from '../css/Stylesheet'; import Stylesheet from '../css/Stylesheet';
import { Node, Parsed, CompileOptions, Warning } from '../interfaces'; 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 { export class Validator {
readonly source: string; readonly source: string;
readonly filename: string; readonly filename: string;
@ -72,7 +60,13 @@ export class Validator {
} }
error(message: string, pos: { start: number, end: number }) { 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 }) { warn(message: string, pos: { start: number, end: number }) {

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

@ -2,7 +2,7 @@
import { assign, createElement, detachNode, init, insertNode, noop, proto } from "svelte/shared.js"; import { assign, createElement, detachNode, init, insertNode, noop, proto } from "svelte/shared.js";
function link(node) { function link(node) {
function onClick(event) { function onClick(event) {
event.preventDefault(); event.preventDefault();
history.pushState(null, null, event.target.href); history.pushState(null, null, event.target.href);

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

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

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

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

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

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

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

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

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

@ -57,4 +57,4 @@ function SvelteComponent(options) {
} }
assign(assign(SvelteComponent.prototype, methods), proto); 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); assign(assign(SvelteComponent.prototype, methods), proto);
setup(SvelteComponent); 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