From e84e9cc038a135444087d7e6691dc86c0d35a7ed Mon Sep 17 00:00:00 2001 From: jason lim <50891910+Sxxov@users.noreply.github.com> Date: Sun, 4 Apr 2021 00:22:22 +0800 Subject: [PATCH 1/2] Implement spread binding --- src/compiler/compile/nodes/Binding.ts | 2 + .../wrappers/InlineComponent/index.ts | 97 +++++-- src/compiler/parse/state/tag.ts | 25 ++ .../samples/binding-spread/input.svelte | 8 + .../parser/samples/binding-spread/output.json | 244 ++++++++++++++++++ .../TextInput.svelte | 6 + .../binding-using-props-spread/_config.js | 14 + .../binding-using-props-spread/main.svelte | 10 + 8 files changed, 380 insertions(+), 26 deletions(-) create mode 100644 test/parser/samples/binding-spread/input.svelte create mode 100644 test/parser/samples/binding-spread/output.json create mode 100644 test/runtime/samples/binding-using-props-spread/TextInput.svelte create mode 100644 test/runtime/samples/binding-using-props-spread/_config.js create mode 100644 test/runtime/samples/binding-using-props-spread/main.svelte diff --git a/src/compiler/compile/nodes/Binding.ts b/src/compiler/compile/nodes/Binding.ts index 8fcc70ded9a0..a0f792485821 100644 --- a/src/compiler/compile/nodes/Binding.ts +++ b/src/compiler/compile/nodes/Binding.ts @@ -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? is_contextual: boolean; is_readonly: boolean; + is_spread: boolean; constructor(component: Component, parent: Element | InlineComponent | Window, scope: TemplateScope, info: TemplateNode) { super(component, parent, scope, info); @@ -42,6 +43,7 @@ export default class Binding extends Node { } this.name = info.name; + this.is_spread = info.modifiers.includes('spread'); this.expression = new Expression(component, this, scope, info.expression); this.raw_expression = clone(info.expression); diff --git a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts index c6299321f4bc..78317e493c5f 100644 --- a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts +++ b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts @@ -18,6 +18,7 @@ import { extract_names } from 'periscopic'; import mark_each_block_bindings from '../shared/mark_each_block_bindings'; import { string_to_member_expression } from '../../../utils/string_to_member_expression'; import SlotTemplate from '../../../nodes/SlotTemplate'; +import Binding from '../../../nodes/Binding'; type SlotDefinition = { block: Block; scope: TemplateScope; get_context?: Node; get_changes?: Node }; @@ -136,7 +137,9 @@ export default class InlineComponentWrapper extends Wrapper { let props; 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 for (const slot of this.slots.keys()) { @@ -199,7 +202,8 @@ export default class InlineComponentWrapper extends Wrapper { updates.push(b`const ${name_changes} = {};`); } - if (this.node.attributes.length) { + if (this.node.attributes.length + || this.node.bindings.length) { if (uses_spread) { 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); }); - this.node.attributes.forEach((attr, i) => { - const { name, dependencies } = attr; + this.node.bindings.forEach(binding => { + 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) ? renderer.dirty(Array.from(dependencies)) @@ -221,17 +235,20 @@ export default class InlineComponentWrapper extends Wrapper { const unchanged = dependencies.size === 0; let change_object; - if (attr.is_spread) { - const value = attr.expression.manipulate(block); + if (node.is_spread) { + const value = node.expression.manipulate(block); initial_props.push(value); let value_object = value; - if (attr.expression.node.type !== 'ObjectExpression') { + if (node.expression.node.type !== 'ObjectExpression') { value_object = x`@get_spread_object(${value})`; } 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); change_object = obj; } @@ -307,19 +324,28 @@ export default class InlineComponentWrapper extends Wrapper { const snippet = binding.expression.manipulate(block); - statements.push(b` - if (${snippet} !== void 0) { - ${props}.${binding.name} = ${snippet}; - }` - ); + if (binding.is_spread) { + updates.push(b` + 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` - if (!${updating} && ${renderer.dirty(Array.from(binding.expression.dependencies))}) { - ${updating} = true; - ${name_changes}.${binding.name} = ${snippet}; - @add_flush_callback(() => ${updating} = false); - } - `); + updates.push(b` + if (!${updating} && ${renderer.dirty(Array.from(binding.expression.dependencies))}) { + ${updating} = true; + ${name_changes}.${binding.name} = ${snippet}; + @add_flush_callback(() => ${updating} = false); + } + `); + } const contextual_dependencies = Array.from(binding.expression.contextual_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); } - const params = [x`#value`]; - const args = [x`#value`]; - if (contextual_dependencies.length > 0) { + if (binding.is_spread) { + lhs = x`${lhs}[#key]`; + } + 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 => { params.push({ type: 'Identifier', @@ -349,12 +382,11 @@ export default class InlineComponentWrapper extends Wrapper { args.push(renderer.reference(name)); }); - block.maintain_context = true; // TODO put this somewhere more logical } block.chunks.init.push(b` - function ${id}(#value) { + function ${id}(${params}) { ${callee}(${args}); } `); @@ -379,6 +411,19 @@ export default class InlineComponentWrapper extends Wrapper { 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}));`; }); diff --git a/src/compiler/parse/state/tag.ts b/src/compiler/parse/state/tag.ts index 799124b2bdf7..e3fa974b7d47 100644 --- a/src/compiler/parse/state/tag.ts +++ b/src/compiler/parse/state/tag.ts @@ -314,6 +314,31 @@ function read_attribute(parser: Parser, unique_names: Set) { if (parser.eat('...')) { 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.eat('}', true); diff --git a/test/parser/samples/binding-spread/input.svelte b/test/parser/samples/binding-spread/input.svelte new file mode 100644 index 000000000000..556248b3cf2f --- /dev/null +++ b/test/parser/samples/binding-spread/input.svelte @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/test/parser/samples/binding-spread/output.json b/test/parser/samples/binding-spread/output.json new file mode 100644 index 000000000000..004c6d20bd14 --- /dev/null +++ b/test/parser/samples/binding-spread/output.json @@ -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" + } + } +} \ No newline at end of file diff --git a/test/runtime/samples/binding-using-props-spread/TextInput.svelte b/test/runtime/samples/binding-using-props-spread/TextInput.svelte new file mode 100644 index 000000000000..97a75c4d1a43 --- /dev/null +++ b/test/runtime/samples/binding-using-props-spread/TextInput.svelte @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/test/runtime/samples/binding-using-props-spread/_config.js b/test/runtime/samples/binding-using-props-spread/_config.js new file mode 100644 index 000000000000..2e5fef1f4ffc --- /dev/null +++ b/test/runtime/samples/binding-using-props-spread/_config.js @@ -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, ` + +

changed

+ `); + } +}; diff --git a/test/runtime/samples/binding-using-props-spread/main.svelte b/test/runtime/samples/binding-using-props-spread/main.svelte new file mode 100644 index 000000000000..78b058947f14 --- /dev/null +++ b/test/runtime/samples/binding-using-props-spread/main.svelte @@ -0,0 +1,10 @@ + + + +

{actualValue.foo}

\ No newline at end of file From 90b9ed830f4f0d201f1c113159fab2df124ba6e9 Mon Sep 17 00:00:00 2001 From: jason lim <50891910+Sxxov@users.noreply.github.com> Date: Sun, 4 Apr 2021 01:11:26 +0800 Subject: [PATCH 2/2] Add docs and POC --- site/content/docs/02-template-syntax.md | 62 +++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/site/content/docs/02-template-syntax.md b/site/content/docs/02-template-syntax.md index 0e0e8e400822..67be6fe95c85 100644 --- a/site/content/docs/02-template-syntax.md +++ b/site/content/docs/02-template-syntax.md @@ -762,6 +762,68 @@ To get a reference to a DOM node, use `bind:this`. ``` +#### [{...bind:*property*}](spread_bind_element_property) + +```sv +{...bind:variable} +``` + +--- + +This type of binding works the same as simply using the spread syntax, but gives the advantages of two-way binding. + +A scenario where one would want this would be for passing binds from a parent to a child component (A → B → C). + +```sv + + + + +``` + +```sv + + + + +``` + +```sv + + + +

foo: {foo}; bar: {bar}; baz: {baz}

+``` #### class:*name*