Merge pull request #1385 from sveltejs/destructuring

Destructuring
pull/1388/head
Rich Harris 7 years ago committed by GitHub
commit 1130556312
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,8 +9,7 @@ export interface BlockOptions {
compiler?: Compiler;
comment?: string;
key?: string;
indexNames?: Map<string, string>;
listNames?: Map<string, string>;
bindings?: Map<string, string>;
dependencies?: Set<string>;
}
@ -23,8 +22,8 @@ export default class Block {
first: string;
dependencies: Set<string>;
indexNames: Map<string, string>;
listNames: Map<string, string>;
bindings: Map<string, string>;
builders: {
init: CodeBuilder;
@ -62,8 +61,7 @@ export default class Block {
this.dependencies = new Set();
this.indexNames = options.indexNames;
this.listNames = options.listNames;
this.bindings = options.bindings;
this.builders = {
init: new CodeBuilder(),

@ -195,14 +195,13 @@ function getEventHandler(
? getTailSnippet(binding.value.node)
: '';
const list = `ctx.${block.listNames.get(name)}`;
const index = `ctx.${block.indexNames.get(name)}`;
const head = block.bindings.get(name);
return {
usesContext: true,
usesState: true,
usesStore: storeDependencies.length > 0,
mutation: `${list}[${index}]${tail} = ${value};`,
mutation: `${head}${tail} = ${value};`,
props: dependencies.map(prop => `${prop}: ctx.${prop}`),
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
};

@ -239,12 +239,11 @@ export default class Component extends Node {
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);
const head = block.bindings.get(key);
const lhs = binding.value.node.type === 'MemberExpression'
? binding.value.snippet
: `ctx.${list}[ctx.${index}]${tail} = childState.${binding.name}`;
: `${head}${tail} = childState.${binding.name}`;
setFromChild = deindent`
${lhs} = childState.${binding.name};

@ -6,6 +6,7 @@ import createDebuggingComment from '../../utils/createDebuggingComment';
import Expression from './shared/Expression';
import mapChildren from './shared/mapChildren';
import TemplateScope from './shared/TemplateScope';
import unpackDestructuring from '../../utils/unpackDestructuring';
export default class EachBlock extends Node {
type: 'EachBlock';
@ -18,7 +19,7 @@ export default class EachBlock extends Node {
context: string;
key: Expression;
scope: TemplateScope;
destructuredContexts: string[];
contexts: Array<{ name: string, tail: string }>;
children: Node[];
else?: ElseBlock;
@ -27,7 +28,7 @@ export default class EachBlock extends Node {
super(compiler, parent, scope, info);
this.expression = new Expression(compiler, this, scope, info.expression);
this.context = info.context;
this.context = info.context.name || 'each'; // TODO this is used to facilitate binding; currently fails with destructuring
this.index = info.index;
this.key = info.key
@ -36,7 +37,12 @@ export default class EachBlock extends Node {
this.scope = scope.child();
this.scope.add(this.context, this.expression.dependencies);
this.contexts = [];
unpackDestructuring(this.contexts, info.context, '');
this.contexts.forEach(context => {
this.scope.add(context.key.name, this.expression.dependencies);
});
if (this.index) {
// index can only change if this is a keyed each block
@ -44,12 +50,6 @@ export default class EachBlock extends Node {
this.scope.add(this.index, dependencies);
}
// TODO more general approach to destructuring
this.destructuredContexts = info.destructuredContexts || [];
this.destructuredContexts.forEach(name => {
this.scope.add(name, this.expression.dependencies);
});
this.children = mapChildren(compiler, this, this.scope, info.children);
this.else = info.else
@ -76,31 +76,28 @@ export default class EachBlock extends Node {
name: this.compiler.getUniqueName('create_each_block'),
key: this.key,
indexNames: new Map(block.indexNames),
listNames: new Map(block.listNames)
bindings: new Map(block.bindings)
});
const listName = this.compiler.getUniqueName('each_value');
this.each_block_value = this.compiler.getUniqueName('each_value');
const indexName = this.index || this.compiler.getUniqueName(`${this.context}_index`);
this.block.indexNames.set(this.context, indexName);
this.block.listNames.set(this.context, listName);
this.contexts.forEach(prop => {
this.block.bindings.set(prop.key.name, `ctx.${this.each_block_value}[ctx.${indexName}]${prop.tail}`);
});
if (this.index) {
this.block.getUniqueName(this.index); // this prevents name collisions (#1254)
}
this.contextProps = [
`${listName}: list`,
`${this.context}: list[i]`,
`${indexName}: i`
];
this.contextProps = this.contexts.map(prop => `${prop.key.name}: list[i]${prop.tail}`);
if (this.destructuredContexts) {
for (let i = 0; i < this.destructuredContexts.length; i += 1) {
this.contextProps.push(`${this.destructuredContexts[i]}: list[i][${i}]`);
}
}
// TODO only add these if necessary
this.contextProps.push(
`${this.each_block_value}: list`,
`${indexName}: i`
);
this.compiler.target.blocks.push(this.block);
this.initChildren(this.block, stripWhitespace, nextSibling);
@ -135,7 +132,6 @@ export default class EachBlock extends Node {
const each = this.var;
const create_each_block = this.block.name;
const each_block_value = this.block.listNames.get(this.context);
const iterations = this.iterations;
const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode();
@ -154,7 +150,6 @@ export default class EachBlock extends Node {
const vars = {
each,
create_each_block,
each_block_value,
length,
iterations,
anchor,
@ -163,7 +158,7 @@ export default class EachBlock extends Node {
const { snippet } = this.expression;
block.builders.init.addLine(`var ${each_block_value} = ${snippet};`);
block.builders.init.addLine(`var ${this.each_block_value} = ${snippet};`);
this.compiler.target.blocks.push(deindent`
function ${this.get_each_context}(ctx, list, i) {
@ -195,7 +190,7 @@ export default class EachBlock extends Node {
// TODO neaten this up... will end up with an empty line in the block
block.builders.init.addBlock(deindent`
if (!${each_block_value}.${length}) {
if (!${this.each_block_value}.${length}) {
${each_block_else} = ${this.else.block.name}(#component, ctx);
${each_block_else}.c();
}
@ -211,9 +206,9 @@ export default class EachBlock extends Node {
if (this.else.block.hasUpdateMethod) {
block.builders.update.addBlock(deindent`
if (!${each_block_value}.${length} && ${each_block_else}) {
if (!${this.each_block_value}.${length} && ${each_block_else}) {
${each_block_else}.p(changed, ctx);
} else if (!${each_block_value}.${length}) {
} else if (!${this.each_block_value}.${length}) {
${each_block_else} = ${this.else.block.name}(#component, ctx);
${each_block_else}.c();
${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor});
@ -225,7 +220,7 @@ export default class EachBlock extends Node {
`);
} else {
block.builders.update.addBlock(deindent`
if (${each_block_value}.${length}) {
if (${this.each_block_value}.${length}) {
if (${each_block_else}) {
${each_block_else}.u();
${each_block_else}.d();
@ -267,7 +262,6 @@ export default class EachBlock extends Node {
{
each,
create_each_block,
each_block_value,
length,
anchor,
mountOrIntro,
@ -295,8 +289,8 @@ export default class EachBlock extends Node {
block.builders.init.addBlock(deindent`
const ${get_key} = ctx => ${this.key.snippet};
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
let child_ctx = ${this.get_each_context}(ctx, ${each_block_value}, #i);
for (var #i = 0; #i < ${this.each_block_value}.${length}; #i += 1) {
let child_ctx = ${this.get_each_context}(ctx, ${this.each_block_value}, #i);
let key = ${get_key}(child_ctx);
${blocks}[#i] = ${lookup}[key] = ${create_each_block}(#component, key, child_ctx);
}
@ -323,9 +317,9 @@ export default class EachBlock extends Node {
const dynamic = this.block.hasUpdateMethod;
block.builders.update.addBlock(deindent`
var ${each_block_value} = ${snippet};
var ${this.each_block_value} = ${snippet};
${blocks} = @updateKeyedEach(${blocks}, #component, changed, ${get_key}, ${dynamic ? '1' : '0'}, ctx, ${each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", ${anchor}, ${this.get_each_context});
${blocks} = @updateKeyedEach(${blocks}, #component, changed, ${get_key}, ${dynamic ? '1' : '0'}, ctx, ${this.each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", ${anchor}, ${this.get_each_context});
`);
if (!parentNode) {
@ -346,7 +340,6 @@ export default class EachBlock extends Node {
snippet: string,
{
create_each_block,
each_block_value,
length,
iterations,
anchor,
@ -356,8 +349,8 @@ export default class EachBlock extends Node {
block.builders.init.addBlock(deindent`
var ${iterations} = [];
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
${iterations}[#i] = ${create_each_block}(#component, ${this.get_each_context}(ctx, ${each_block_value}, #i));
for (var #i = 0; #i < ${this.each_block_value}.${length}; #i += 1) {
${iterations}[#i] = ${create_each_block}(#component, ${this.get_each_context}(ctx, ${this.each_block_value}, #i));
}
`);
@ -445,15 +438,15 @@ export default class EachBlock extends Node {
${iterations}[#i].u();
${iterations}[#i].d();
}
${iterations}.length = ${each_block_value}.${length};
${iterations}.length = ${this.each_block_value}.${length};
`;
block.builders.update.addBlock(deindent`
if (${condition}) {
${each_block_value} = ${snippet};
${this.each_block_value} = ${snippet};
for (var #i = ${start}; #i < ${each_block_value}.${length}; #i += 1) {
const child_ctx = ${this.get_each_context}(ctx, ${each_block_value}, #i);
for (var #i = ${start}; #i < ${this.each_block_value}.${length}; #i += 1) {
const child_ctx = ${this.get_each_context}(ctx, ${this.each_block_value}, #i);
${forLoopBody}
}
@ -481,8 +474,7 @@ export default class EachBlock extends Node {
const { compiler } = this;
const { snippet } = this.expression;
const props = [`${this.context}: item`]
.concat(this.destructuredContexts.map((name, i) => `${name}: item[${i}]`));
const props = this.contexts.map(prop => `${prop.key.name}: item${prop.tail}`);
const getContext = this.index
? `(item, i) => Object.assign({}, ctx, { ${props.join(', ')}, ${this.index}: i })`

@ -23,8 +23,7 @@ export default class Fragment extends Node {
name: '@create_main_fragment',
key: null,
indexNames: new Map(),
listNames: new Map(),
bindings: new Map(),
dependencies: new Set(),
});

@ -0,0 +1,119 @@
import { Parser } from '../index';
type Identifier = {
start: number;
end: number;
type: 'Identifier';
name: string;
};
type Property = {
start: number;
end: number;
type: 'Property';
key: Identifier;
value: Context;
};
type Context = {
start: number;
end: number;
type: 'Identifier' | 'ArrayPattern' | 'ObjectPattern';
name?: string;
elements?: Context[];
properties?: Property[];
}
function errorOnAssignmentPattern(parser: Parser) {
if (parser.eat('=')) {
parser.error({
code: 'invalid-assignment-pattern',
message: 'Assignment patterns are not supported'
}, parser.index - 1);
}
}
export default function readContext(parser: Parser) {
const context: Context = {
start: parser.index,
end: null,
type: null
};
if (parser.eat('[')) {
context.type = 'ArrayPattern';
context.elements = [];
do {
parser.allowWhitespace();
if (parser.template[parser.index] === ',') {
context.elements.push(null);
} else {
context.elements.push(readContext(parser));
parser.allowWhitespace();
}
} while (parser.eat(','));
errorOnAssignmentPattern(parser);
parser.eat(']', true);
}
else if (parser.eat('{')) {
context.type = 'ObjectPattern';
context.properties = [];
do {
parser.allowWhitespace();
const start = parser.index;
const name = parser.readIdentifier();
const key: Identifier = {
start,
end: parser.index,
type: 'Identifier',
name
};
parser.allowWhitespace();
const value = parser.eat(':')
? (parser.allowWhitespace(), readContext(parser))
: key;
const property: Property = {
start,
end: value.end,
type: 'Property',
key,
value
};
context.properties.push(property);
parser.allowWhitespace();
} while (parser.eat(','));
errorOnAssignmentPattern(parser);
parser.eat('}', true);
}
else {
const name = parser.readIdentifier();
if (name) {
context.type = 'Identifier';
context.end = parser.index;
context.name = name;
}
else {
parser.error({
code: 'invalid-context',
message: 'Expected a name, array pattern or object pattern'
});
}
errorOnAssignmentPattern(parser);
}
return context;
}

@ -1,3 +1,4 @@
import readContext from '../read/context';
import readExpression from '../read/expression';
import { whitespace } from '../../utils/patterns';
import { trimStart, trimEnd } from '../../utils/trim';
@ -248,40 +249,7 @@ export default function mustache(parser: Parser) {
parser.eat('as', true);
parser.requireWhitespace();
if (parser.eat('[')) {
parser.allowWhitespace();
block.destructuredContexts = [];
do {
parser.allowWhitespace();
const destructuredContext = parser.readIdentifier();
if (!destructuredContext) parser.error({
code: `expected-name`,
message: `Expected name`
});
block.destructuredContexts.push(destructuredContext);
parser.allowWhitespace();
} while (parser.eat(','));
if (!block.destructuredContexts.length) parser.error({
code: `expected-name`,
message: `Expected name`
});
block.context = block.destructuredContexts.join('_');
parser.allowWhitespace();
parser.eat(']', true);
} else {
block.context = parser.readIdentifier();
if (!block.context) parser.error({
code: `expected-name`,
message: `Expected name`
});
}
block.context = readContext(parser);
parser.allowWhitespace();

@ -0,0 +1,22 @@
export default function unpackDestructuring(
contexts: Array<{ name: string, tail: string }>,
node: Node,
tail: string
) {
if (!node) return;
if (node.type === 'Identifier') {
contexts.push({
key: node,
tail
});
} else if (node.type === 'ArrayPattern') {
node.elements.forEach((element, i) => {
unpackDestructuring(contexts, element, `${tail}[${i}]`);
});
} else if (node.type === 'ObjectPattern') {
node.properties.forEach((property) => {
unpackDestructuring(contexts, property.value, `${tail}.${property.key.name}`);
});
}
}

@ -8,6 +8,7 @@ import fuzzymatch from '../utils/fuzzymatch'
import flattenReference from '../../utils/flattenReference';
import { Validator } from '../index';
import { Node } from '../../interfaces';
import unpackDestructuring from '../../utils/unpackDestructuring';
function isEmptyBlock(node: Node) {
if (!/Block$/.test(node.type) || !node.children) return false;
@ -60,19 +61,17 @@ export default function validateHtml(validator: Validator, html: Node) {
}
else if (node.type === 'EachBlock') {
if (validator.helpers.has(node.context)) {
let c: number = node.expression.end;
const contexts = [];
unpackDestructuring(contexts, node.context, '');
// find start of context
while (/\s/.test(validator.source[c])) c += 1;
c += 2;
while (/\s/.test(validator.source[c])) c += 1;
validator.warn({ start: c, end: c + node.context.length }, {
contexts.forEach(prop => {
if (validator.helpers.has(prop.key.name)) {
validator.warn(prop.key, {
code: `each-context-clash`,
message: `Context clashes with a helper. Rename one or the other to eliminate any ambiguity`
});
}
});
}
if (validator.options.dev && isEmptyBlock(node)) {

@ -250,8 +250,8 @@ function create_each_block(component, ctx) {
function get_each_context(ctx, list, i) {
return assign(assign({}, ctx), {
each_value: list,
node: list[i],
each_value: list,
node_index: i
});
}

@ -98,8 +98,8 @@ function create_each_block(component, ctx) {
function get_each_context(ctx, list, i) {
return assign(assign({}, ctx), {
each_value: list,
node: list[i],
each_value: list,
node_index: i
});
}

@ -297,8 +297,8 @@ function create_each_block(component, ctx) {
function get_each_context(ctx, list, i) {
return assign(assign({}, ctx), {
each_value: list,
comment: list[i],
each_value: list,
i: i
});
}

@ -143,8 +143,8 @@ function create_each_block(component, ctx) {
function get_each_context(ctx, list, i) {
return assign(assign({}, ctx), {
each_value: list,
comment: list[i],
each_value: list,
i: i
});
}

@ -1,5 +1,4 @@
{
"hash": "gtdm5e",
"html": {
"start": 0,
"end": 62,
@ -54,11 +53,25 @@
]
}
],
"destructuredContexts": [
"key",
"value"
],
"context": "key_value"
"context": {
"start": 18,
"end": null,
"type": "ArrayPattern",
"elements": [
{
"start": 19,
"end": 22,
"type": "Identifier",
"name": "key"
},
{
"start": 24,
"end": 29,
"type": "Identifier",
"name": "value"
}
]
}
}
]
},

@ -1,5 +1,4 @@
{
"hash": "ljl07n",
"html": {
"start": 0,
"end": 77,
@ -37,7 +36,12 @@
]
}
],
"context": "animal",
"context": {
"start": 18,
"end": 24,
"type": "Identifier",
"name": "animal"
},
"else": {
"start": 50,
"end": 70,

@ -1,5 +1,4 @@
{
"hash": "1143n2g",
"html": {
"start": 0,
"end": 58,
@ -54,7 +53,12 @@
]
}
],
"context": "animal",
"context": {
"start": 18,
"end": 24,
"type": "Identifier",
"name": "animal"
},
"index": "i"
}
]

@ -36,7 +36,12 @@
]
}
],
"context": "todo",
"context": {
"start": 16,
"end": 20,
"type": "Identifier",
"name": "todo"
},
"key": {
"type": "MemberExpression",
"start": 22,

@ -1,5 +1,4 @@
{
"hash": "mzeq0s",
"html": {
"start": 0,
"end": 50,
@ -37,7 +36,12 @@
]
}
],
"context": "animal"
"context": {
"start": 18,
"end": 24,
"type": "Identifier",
"name": "animal"
}
}
]
},

@ -1,5 +1,4 @@
{
"hash": "8weqxs",
"html": {
"start": 0,
"end": 41,
@ -37,7 +36,12 @@
]
}
],
"context": "𐊧"
"context": {
"start": 17,
"end": 19,
"type": "Identifier",
"name": "𐊧"
}
}
]
},

@ -0,0 +1,20 @@
export default {
data: {
animalPawsEntries: [
['raccoon', 'hands'],
['eagle', 'wings']
]
},
html: `
<p>hands</p>
<p>wings</p>
`,
test ( assert, component, target ) {
component.set({ animalPawsEntries: [['foo', 'bar']] });
assert.htmlEqual( target.innerHTML, `
<p>bar</p>
`);
},
};

@ -0,0 +1,3 @@
{#each animalPawsEntries as [, pawType]}
<p>{pawType}</p>
{/each}

@ -0,0 +1,39 @@
export default {
data: {
people: [{ name: { first: 'Doctor', last: 'Who' } }],
},
html: `
<input>
<input>
<p>Doctor Who</p>
`,
test(assert, component, target, window) {
const inputs = target.querySelectorAll('input');
inputs[1].value = 'Oz';
inputs[1].dispatchEvent(new window.Event('input'));
const { people } = component.get();
assert.deepEqual(people, [
{ name: { first: 'Doctor', last: 'Oz' } }
]);
assert.htmlEqual(target.innerHTML, `
<input>
<input>
<p>Doctor Oz</p>
`);
people[0].name.first = 'Frank';
component.set({ people });
assert.htmlEqual(target.innerHTML, `
<input>
<input>
<p>Frank Oz</p>
`);
},
};

@ -0,0 +1,5 @@
{#each people as { name: { first: f, last: l } } }
<input bind:value=f>
<input bind:value=l>
<p>{f} {l}</p>
{/each}

@ -0,0 +1,22 @@
export default {
data: {
animalPawsEntries: [
{ animal: 'raccoon', pawType: 'hands' },
{ animal: 'eagle', pawType: 'wings' }
]
},
html: `
<p>raccoon: hands</p>
<p>eagle: wings</p>
`,
test ( assert, component, target ) {
component.set({
animalPawsEntries: [{ animal: 'cow', pawType: 'hooves' }]
});
assert.htmlEqual( target.innerHTML, `
<p>cow: hooves</p>
`);
},
};

@ -0,0 +1,3 @@
{#each animalPawsEntries as { animal, pawType } }
<p>{animal}: {pawType}</p>
{/each}
Loading…
Cancel
Save