diff --git a/src/generators/dom/visitors/Component/Binding.ts b/src/generators/dom/visitors/Component/Binding.ts index 724901dea1..9c30f24dc7 100644 --- a/src/generators/dom/visitors/Component/Binding.ts +++ b/src/generators/dom/visitors/Component/Binding.ts @@ -39,7 +39,7 @@ export default function visitBinding ( generator: DomGenerator, block: Block, st prop }); - const setter = getSetter({ block, name, context: '_context', attribute, dependencies, value: 'value' }); + const setter = getSetter({ block, name, snippet, context: '_context', attribute, dependencies, value: 'value' }); generator.hasComplexBindings = true; diff --git a/src/generators/dom/visitors/Element/Binding.ts b/src/generators/dom/visitors/Element/Binding.ts index 4b0cfc3d09..d297bd1beb 100644 --- a/src/generators/dom/visitors/Element/Binding.ts +++ b/src/generators/dom/visitors/Element/Binding.ts @@ -7,9 +7,16 @@ import Block from '../../Block'; import { Node } from '../../../../interfaces'; import { State } from '../../interfaces'; +function getObject ( node ) { + // TODO validation should ensure this is an Identifier or a MemberExpression + while ( node.type === 'MemberExpression' ) node = node.object; + return node; +} + export default function visitBinding ( generator: DomGenerator, block: Block, state: State, node: Node, attribute: Node ) { - const { name, parts } = flattenReference( attribute.value ); - const { snippet, contexts, dependencies } = block.contextualise( attribute.value ); + const { name } = getObject( attribute.value ); + const { snippet, contexts } = block.contextualise( attribute.value ); + const dependencies = block.contextDependencies.has( name ) ? block.contextDependencies.get( name ) : [ name ]; if ( dependencies.length > 1 ) throw new Error( 'An unexpected situation arose. Please raise an issue at https://github.com/sveltejs/svelte/issues — thanks!' ); @@ -21,10 +28,10 @@ export default function visitBinding ( generator: DomGenerator, block: Block, st const handler = block.getUniqueName( `${state.parentNode}_${eventName}_handler` ); const isMultipleSelect = node.name === 'select' && node.attributes.find( ( attr: Node ) => attr.name.toLowerCase() === 'multiple' ); // TODO use getStaticAttributeValue const type = getStaticAttributeValue( node, 'type' ); - const bindingGroup = attribute.name === 'group' ? getBindingGroup( generator, parts.join( '.' ) ) : null; + const bindingGroup = attribute.name === 'group' ? getBindingGroup( generator, attribute.value ) : null; const value = getBindingValue( generator, block, state, node, attribute, isMultipleSelect, bindingGroup, type ); - let setter = getSetter({ block, name, context: '_svelte', attribute, dependencies, value }); + let setter = getSetter({ block, name, snippet, context: '_svelte', attribute, dependencies, value }); let updateElement = `${state.parentNode}.${attribute.name} = ${snippet};`; const lock = block.alias( `${state.parentNode}_updating` ); let updateCondition = `!${lock}`; @@ -190,7 +197,10 @@ function getBindingValue ( generator: DomGenerator, block: Block, state: State, return `${state.parentNode}.${attribute.name}`; } -function getBindingGroup ( generator: DomGenerator, keypath: string ) { +function getBindingGroup ( generator: DomGenerator, value: Node ) { + const { parts } = flattenReference( value ); // TODO handle cases involving computed member expressions + const keypath = parts.join( '.' ); + // TODO handle contextual bindings — `keypath` should include unique ID of // each block that provides context let index = generator.bindingGroups.indexOf( keypath ); diff --git a/src/generators/dom/visitors/shared/binding/getSetter.ts b/src/generators/dom/visitors/shared/binding/getSetter.ts index 14c3799280..11028915e4 100644 --- a/src/generators/dom/visitors/shared/binding/getSetter.ts +++ b/src/generators/dom/visitors/shared/binding/getSetter.ts @@ -1,14 +1,16 @@ import deindent from '../../../../../utils/deindent.js'; -export default function getSetter ({ block, name, context, attribute, dependencies, value }) { +export default function getSetter ({ block, name, snippet, context, attribute, dependencies, value }) { const tail = attribute.value.type === 'MemberExpression' ? getTailSnippet( attribute.value ) : ''; if ( block.contexts.has( name ) ) { const prop = dependencies[0]; + const computed = isComputed( attribute.value ); return deindent` var list = this.${context}.${block.listNames.get( name )}; var index = this.${context}.${block.indexNames.get( name )}; + ${computed && `var state = ${block.component}.get();`} list[index]${tail} = ${value}; ${block.component}._set({ ${prop}: ${block.component}.get( '${prop}' ) }); @@ -19,9 +21,9 @@ export default function getSetter ({ block, name, context, attribute, dependenci const alias = block.alias( name ); return deindent` - var ${alias} = ${block.component}.get( '${name}' ); - ${alias}${tail} = ${value}; - ${block.component}._set({ ${name}: ${alias} }); + var state = ${block.component}.get(); + ${snippet} = ${value}; + ${block.component}._set({ ${name}: state.${name} }); `; } @@ -35,3 +37,12 @@ function getTailSnippet ( node ) { return `[✂${start}-${end}✂]`; } + +function isComputed ( node ) { + while ( node.type === 'MemberExpression' ) { + if ( node.computed ) return true; + node = node.object; + } + + return false; +} \ No newline at end of file diff --git a/test/runtime/samples/binding-input-text-deep-computed-dynamic/_config.js b/test/runtime/samples/binding-input-text-deep-computed-dynamic/_config.js new file mode 100644 index 0000000000..de74cba3c4 --- /dev/null +++ b/test/runtime/samples/binding-input-text-deep-computed-dynamic/_config.js @@ -0,0 +1,55 @@ +export default { + data: { + prop: 'bar', + obj: { + foo: 'a', + bar: 'b', + baz: 'c' + } + }, + + html: ` + +
{"foo":"a","bar":"b","baz":"c"}
+ `, + + test ( assert, component, target, window ) { + const input = target.querySelector( 'input' ); + const event = new window.Event( 'input' ); + + assert.equal( input.value, 'b' ); + + // edit bar + input.value = 'e'; + input.dispatchEvent( event ); + + assert.htmlEqual( target.innerHTML, ` + +
{"foo":"a","bar":"e","baz":"c"}
+ ` ); + + // edit baz + component.set({ prop: 'baz' }); + assert.equal( input.value, 'c' ); + + input.value = 'f'; + input.dispatchEvent( event ); + + assert.htmlEqual( target.innerHTML, ` + +
{"foo":"a","bar":"e","baz":"f"}
+ ` ); + + // edit foo + component.set({ prop: 'foo' }); + assert.equal( input.value, 'a' ); + + input.value = 'd'; + input.dispatchEvent( event ); + + assert.htmlEqual( target.innerHTML, ` + +
{"foo":"d","bar":"e","baz":"f"}
+ ` ); + } +}; diff --git a/test/runtime/samples/binding-input-text-deep-computed-dynamic/main.html b/test/runtime/samples/binding-input-text-deep-computed-dynamic/main.html new file mode 100644 index 0000000000..3a95b383e4 --- /dev/null +++ b/test/runtime/samples/binding-input-text-deep-computed-dynamic/main.html @@ -0,0 +1,2 @@ + +
{{JSON.stringify(obj)}}
\ No newline at end of file diff --git a/test/runtime/samples/binding-input-text-deep-computed/_config.js b/test/runtime/samples/binding-input-text-deep-computed/_config.js new file mode 100644 index 0000000000..597395ed52 --- /dev/null +++ b/test/runtime/samples/binding-input-text-deep-computed/_config.js @@ -0,0 +1,30 @@ +export default { + data: { + prop: 'name', + user: { + name: 'alice' + } + }, + + html: `\n

hello alice

`, + + test ( assert, component, target, window ) { + const input = target.querySelector( 'input' ); + + assert.equal( input.value, 'alice' ); + + const event = new window.Event( 'input' ); + + input.value = 'bob'; + input.dispatchEvent( event ); + + assert.equal( target.innerHTML, `\n

hello bob

` ); + + const user = component.get( 'user' ); + user.name = 'carol'; + + component.set({ user }); + assert.equal( input.value, 'carol' ); + assert.equal( target.innerHTML, `\n

hello carol

` ); + } +}; diff --git a/test/runtime/samples/binding-input-text-deep-computed/main.html b/test/runtime/samples/binding-input-text-deep-computed/main.html new file mode 100644 index 0000000000..281849a4d0 --- /dev/null +++ b/test/runtime/samples/binding-input-text-deep-computed/main.html @@ -0,0 +1,2 @@ + +

hello {{user.name}}

diff --git a/test/runtime/samples/binding-input-text-deep-contextual-computed-dynamic/_config.js b/test/runtime/samples/binding-input-text-deep-contextual-computed-dynamic/_config.js new file mode 100644 index 0000000000..5a16a691bf --- /dev/null +++ b/test/runtime/samples/binding-input-text-deep-contextual-computed-dynamic/_config.js @@ -0,0 +1,55 @@ +export default { + data: { + prop: 'bar', + objects: [{ + foo: 'a', + bar: 'b', + baz: 'c' + }] + }, + + html: ` + +
{"foo":"a","bar":"b","baz":"c"}
+ `, + + test ( assert, component, target, window ) { + const input = target.querySelector( 'input' ); + const event = new window.Event( 'input' ); + + assert.equal( input.value, 'b' ); + + // edit bar + input.value = 'e'; + input.dispatchEvent( event ); + + assert.htmlEqual( target.innerHTML, ` + +
{"foo":"a","bar":"e","baz":"c"}
+ ` ); + + // edit baz + component.set({ prop: 'baz' }); + assert.equal( input.value, 'c' ); + + input.value = 'f'; + input.dispatchEvent( event ); + + assert.htmlEqual( target.innerHTML, ` + +
{"foo":"a","bar":"e","baz":"f"}
+ ` ); + + // edit foo + component.set({ prop: 'foo' }); + assert.equal( input.value, 'a' ); + + input.value = 'd'; + input.dispatchEvent( event ); + + assert.htmlEqual( target.innerHTML, ` + +
{"foo":"d","bar":"e","baz":"f"}
+ ` ); + } +}; diff --git a/test/runtime/samples/binding-input-text-deep-contextual-computed-dynamic/main.html b/test/runtime/samples/binding-input-text-deep-contextual-computed-dynamic/main.html new file mode 100644 index 0000000000..b2f36850f4 --- /dev/null +++ b/test/runtime/samples/binding-input-text-deep-contextual-computed-dynamic/main.html @@ -0,0 +1,4 @@ +{{#each objects as obj}} + +
{{JSON.stringify(obj)}}
+{{/each}} \ No newline at end of file