Implement spread binding

pull/6162/head
jason lim 5 years ago
parent c7e820e08f
commit e84e9cc038

@ -30,6 +30,7 @@ export default class Binding extends Node {
raw_expression: ESTreeNode; // TODO exists only for bind:this — is there a more elegant solution? raw_expression: ESTreeNode; // TODO exists only for bind:this — is there a more elegant solution?
is_contextual: boolean; is_contextual: boolean;
is_readonly: boolean; is_readonly: boolean;
is_spread: boolean;
constructor(component: Component, parent: Element | InlineComponent | Window, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: Element | InlineComponent | Window, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info); super(component, parent, scope, info);
@ -42,6 +43,7 @@ export default class Binding extends Node {
} }
this.name = info.name; this.name = info.name;
this.is_spread = info.modifiers.includes('spread');
this.expression = new Expression(component, this, scope, info.expression); this.expression = new Expression(component, this, scope, info.expression);
this.raw_expression = clone(info.expression); this.raw_expression = clone(info.expression);

@ -18,6 +18,7 @@ import { extract_names } from 'periscopic';
import mark_each_block_bindings from '../shared/mark_each_block_bindings'; import mark_each_block_bindings from '../shared/mark_each_block_bindings';
import { string_to_member_expression } from '../../../utils/string_to_member_expression'; import { string_to_member_expression } from '../../../utils/string_to_member_expression';
import SlotTemplate from '../../../nodes/SlotTemplate'; import SlotTemplate from '../../../nodes/SlotTemplate';
import Binding from '../../../nodes/Binding';
type SlotDefinition = { block: Block; scope: TemplateScope; get_context?: Node; get_changes?: Node }; type SlotDefinition = { block: Block; scope: TemplateScope; get_context?: Node; get_changes?: Node };
@ -136,7 +137,9 @@ export default class InlineComponentWrapper extends Wrapper {
let props; let props;
const name_changes = block.get_unique_name(`${name.name}_changes`); const name_changes = block.get_unique_name(`${name.name}_changes`);
const uses_spread = !!this.node.attributes.find(a => a.is_spread); const attributes_uses_spread = !!this.node.attributes.find(a => a.is_spread);
const bindings_uses_spread = !!this.node.bindings.find(b => b.is_spread);
const uses_spread = attributes_uses_spread || bindings_uses_spread;
// removing empty slot // removing empty slot
for (const slot of this.slots.keys()) { for (const slot of this.slots.keys()) {
@ -199,7 +202,8 @@ export default class InlineComponentWrapper extends Wrapper {
updates.push(b`const ${name_changes} = {};`); updates.push(b`const ${name_changes} = {};`);
} }
if (this.node.attributes.length) { if (this.node.attributes.length
|| this.node.bindings.length) {
if (uses_spread) { if (uses_spread) {
const levels = block.get_unique_name(`${this.var.name}_spread_levels`); const levels = block.get_unique_name(`${this.var.name}_spread_levels`);
@ -212,8 +216,18 @@ export default class InlineComponentWrapper extends Wrapper {
add_to_set(all_dependencies, attr.dependencies); add_to_set(all_dependencies, attr.dependencies);
}); });
this.node.attributes.forEach((attr, i) => { this.node.bindings.forEach(binding => {
const { name, dependencies } = attr; add_to_set(all_dependencies, binding.expression.dependencies);
});
[
...this.node.attributes,
...this.node.bindings.filter(binding => binding.is_spread)
].forEach((node: Attribute | Binding, i) => {
const { name } = node;
const dependencies = node.type === 'Attribute'
? node.dependencies
: node.expression.dependencies;
const condition = dependencies.size > 0 && (dependencies.size !== all_dependencies.size) const condition = dependencies.size > 0 && (dependencies.size !== all_dependencies.size)
? renderer.dirty(Array.from(dependencies)) ? renderer.dirty(Array.from(dependencies))
@ -221,17 +235,20 @@ export default class InlineComponentWrapper extends Wrapper {
const unchanged = dependencies.size === 0; const unchanged = dependencies.size === 0;
let change_object; let change_object;
if (attr.is_spread) { if (node.is_spread) {
const value = attr.expression.manipulate(block); const value = node.expression.manipulate(block);
initial_props.push(value); initial_props.push(value);
let value_object = value; let value_object = value;
if (attr.expression.node.type !== 'ObjectExpression') { if (node.expression.node.type !== 'ObjectExpression') {
value_object = x`@get_spread_object(${value})`; value_object = x`@get_spread_object(${value})`;
} }
change_object = value_object; change_object = value_object;
} else { }
const obj = x`{ ${name}: ${attr.get_value(block)} }`;
if (!node.is_spread
&& node.type === 'Attribute') {
const obj = x`{ ${name}: ${node.get_value(block)} }`;
initial_props.push(obj); initial_props.push(obj);
change_object = obj; change_object = obj;
} }
@ -307,19 +324,28 @@ export default class InlineComponentWrapper extends Wrapper {
const snippet = binding.expression.manipulate(block); const snippet = binding.expression.manipulate(block);
statements.push(b` if (binding.is_spread) {
if (${snippet} !== void 0) { updates.push(b`
${props}.${binding.name} = ${snippet}; if (!${updating} && ${renderer.dirty(Array.from(binding.expression.dependencies))}) {
}` ${updating} = true;
); @add_flush_callback(() => ${updating} = false);
}
`);
} else {
statements.push(b`
if (${snippet} !== void 0) {
${props}.${binding.name} = ${snippet};
}`
);
updates.push(b` updates.push(b`
if (!${updating} && ${renderer.dirty(Array.from(binding.expression.dependencies))}) { if (!${updating} && ${renderer.dirty(Array.from(binding.expression.dependencies))}) {
${updating} = true; ${updating} = true;
${name_changes}.${binding.name} = ${snippet}; ${name_changes}.${binding.name} = ${snippet};
@add_flush_callback(() => ${updating} = false); @add_flush_callback(() => ${updating} = false);
} }
`); `);
}
const contextual_dependencies = Array.from(binding.expression.contextual_dependencies); const contextual_dependencies = Array.from(binding.expression.contextual_dependencies);
const dependencies = Array.from(binding.expression.dependencies); const dependencies = Array.from(binding.expression.dependencies);
@ -335,10 +361,17 @@ export default class InlineComponentWrapper extends Wrapper {
contextual_dependencies.push(object.name, property.name); contextual_dependencies.push(object.name, property.name);
} }
const params = [x`#value`]; if (binding.is_spread) {
const args = [x`#value`]; lhs = x`${lhs}[#key]`;
if (contextual_dependencies.length > 0) { }
const params = binding.is_spread
? [x`#key`, x`#value`]
: [x`#value`];
const args = binding.is_spread
? [x`#key`, x`#value`]
: [x`#value`];
if (contextual_dependencies.length > 0) {
contextual_dependencies.forEach(name => { contextual_dependencies.forEach(name => {
params.push({ params.push({
type: 'Identifier', type: 'Identifier',
@ -349,12 +382,11 @@ export default class InlineComponentWrapper extends Wrapper {
args.push(renderer.reference(name)); args.push(renderer.reference(name));
}); });
block.maintain_context = true; // TODO put this somewhere more logical block.maintain_context = true; // TODO put this somewhere more logical
} }
block.chunks.init.push(b` block.chunks.init.push(b`
function ${id}(#value) { function ${id}(${params}) {
${callee}(${args}); ${callee}(${args});
} }
`); `);
@ -379,6 +411,19 @@ export default class InlineComponentWrapper extends Wrapper {
component.partly_hoisted.push(body); component.partly_hoisted.push(body);
if (binding.is_spread) {
return b`
@binding_callbacks.push(
() => Object
.keys(
#ctx[${renderer.context_lookup.get(dependencies[0]).index.value}]
)
.forEach(
#key => @bind(${this.var}, #key, ${id}.bind(undefined, #key))
)
);`;
}
return b`@binding_callbacks.push(() => @bind(${this.var}, '${binding.name}', ${id}));`; return b`@binding_callbacks.push(() => @bind(${this.var}, '${binding.name}', ${id}));`;
}); });

@ -314,6 +314,31 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
if (parser.eat('...')) { if (parser.eat('...')) {
const expression = read_expression(parser); const expression = read_expression(parser);
if (parser.eat(':')
&& expression.type === 'Identifier'
&& expression.name === 'bind') {
const bind_expression = read_expression(parser);
if (bind_expression.type === 'Identifier') {
parser.allow_whitespace();
parser.eat('}', true);
return {
start,
end: parser.index,
type: 'Binding',
name: bind_expression.name,
modifiers: ['spread'],
expression: bind_expression
};
} else {
parser.error({
code: 'unexpected-token',
message: 'Expected identifier'
}, parser.index);
}
}
parser.allow_whitespace(); parser.allow_whitespace();
parser.eat('}', true); parser.eat('}', true);

@ -0,0 +1,8 @@
<script>
let item = {
prop1: 'foo',
prop2: 'bar'
};
</script>
<Widget {...bind:item} />

@ -0,0 +1,244 @@
{
"html": {
"start": 69,
"end": 94,
"type": "Fragment",
"children": [
{
"start": 67,
"end": 69,
"type": "Text",
"raw": "\n\n",
"data": "\n\n"
},
{
"start": 69,
"end": 94,
"type": "InlineComponent",
"name": "Widget",
"attributes": [
{
"start": 77,
"end": 91,
"type": "Binding",
"name": "item",
"modifiers": [
"spread"
],
"expression": {
"type": "Identifier",
"start": 86,
"end": 90,
"loc": {
"start": {
"line": 8,
"column": 17
},
"end": {
"line": 8,
"column": 21
}
},
"name": "item"
}
}
],
"children": []
}
]
},
"instance": {
"type": "Script",
"start": 0,
"end": 67,
"context": "default",
"content": {
"type": "Program",
"start": 8,
"end": 58,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 6,
"column": 0
}
},
"body": [
{
"type": "VariableDeclaration",
"start": 10,
"end": 57,
"loc": {
"start": {
"line": 2,
"column": 1
},
"end": {
"line": 5,
"column": 3
}
},
"declarations": [
{
"type": "VariableDeclarator",
"start": 14,
"end": 56,
"loc": {
"start": {
"line": 2,
"column": 5
},
"end": {
"line": 5,
"column": 2
}
},
"id": {
"type": "Identifier",
"start": 14,
"end": 18,
"loc": {
"start": {
"line": 2,
"column": 5
},
"end": {
"line": 2,
"column": 9
}
},
"name": "item"
},
"init": {
"type": "ObjectExpression",
"start": 21,
"end": 56,
"loc": {
"start": {
"line": 2,
"column": 12
},
"end": {
"line": 5,
"column": 2
}
},
"properties": [
{
"type": "Property",
"start": 25,
"end": 37,
"loc": {
"start": {
"line": 3,
"column": 2
},
"end": {
"line": 3,
"column": 14
}
},
"method": false,
"shorthand": false,
"computed": false,
"key": {
"type": "Identifier",
"start": 25,
"end": 30,
"loc": {
"start": {
"line": 3,
"column": 2
},
"end": {
"line": 3,
"column": 7
}
},
"name": "prop1"
},
"value": {
"type": "Literal",
"start": 32,
"end": 37,
"loc": {
"start": {
"line": 3,
"column": 9
},
"end": {
"line": 3,
"column": 14
}
},
"value": "foo",
"raw": "'foo'"
},
"kind": "init"
},
{
"type": "Property",
"start": 41,
"end": 53,
"loc": {
"start": {
"line": 4,
"column": 2
},
"end": {
"line": 4,
"column": 14
}
},
"method": false,
"shorthand": false,
"computed": false,
"key": {
"type": "Identifier",
"start": 41,
"end": 46,
"loc": {
"start": {
"line": 4,
"column": 2
},
"end": {
"line": 4,
"column": 7
}
},
"name": "prop2"
},
"value": {
"type": "Literal",
"start": 48,
"end": 53,
"loc": {
"start": {
"line": 4,
"column": 9
},
"end": {
"line": 4,
"column": 14
}
},
"value": "bar",
"raw": "'bar'"
},
"kind": "init"
}
]
}
}
],
"kind": "let"
}
],
"sourceType": "module"
}
}
}

@ -0,0 +1,6 @@
<script>
export let foo;
const x = $$props;
</script>
<input bind:value={foo}>

@ -0,0 +1,14 @@
export default {
async test({ assert, target, window }) {
const input = target.querySelector('input');
const event = new window.Event('input');
input.value = 'changed';
await input.dispatchEvent(event);
assert.htmlEqual(target.innerHTML, `
<input>
<p>changed</p>
`);
}
};

@ -0,0 +1,10 @@
<script>
import Input from './TextInput.svelte';
export let actualValue = {
foo: 1,
bar: 2
};
</script>
<Input {...bind:actualValue} />
<p>{actualValue.foo}</p>
Loading…
Cancel
Save