support binding to computed member expressions (fixes #602)

pull/605/head
Rich Harris 8 years ago
parent 79d3c44785
commit 2a92b36472

@ -39,7 +39,7 @@ export default function visitBinding ( generator: DomGenerator, block: Block, st
prop 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; generator.hasComplexBindings = true;

@ -7,9 +7,16 @@ import Block from '../../Block';
import { Node } from '../../../../interfaces'; import { Node } from '../../../../interfaces';
import { State } 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 ) { export default function visitBinding ( generator: DomGenerator, block: Block, state: State, node: Node, attribute: Node ) {
const { name, parts } = flattenReference( attribute.value ); const { name } = getObject( attribute.value );
const { snippet, contexts, dependencies } = block.contextualise( 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!' ); 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 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 isMultipleSelect = node.name === 'select' && node.attributes.find( ( attr: Node ) => attr.name.toLowerCase() === 'multiple' ); // TODO use getStaticAttributeValue
const type = getStaticAttributeValue( node, 'type' ); 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 ); 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};`; let updateElement = `${state.parentNode}.${attribute.name} = ${snippet};`;
const lock = block.alias( `${state.parentNode}_updating` ); const lock = block.alias( `${state.parentNode}_updating` );
let updateCondition = `!${lock}`; let updateCondition = `!${lock}`;
@ -190,7 +197,10 @@ function getBindingValue ( generator: DomGenerator, block: Block, state: State,
return `${state.parentNode}.${attribute.name}`; 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 // TODO handle contextual bindings — `keypath` should include unique ID of
// each block that provides context // each block that provides context
let index = generator.bindingGroups.indexOf( keypath ); let index = generator.bindingGroups.indexOf( keypath );

@ -1,14 +1,16 @@
import deindent from '../../../../../utils/deindent.js'; 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 ) : ''; const tail = attribute.value.type === 'MemberExpression' ? getTailSnippet( attribute.value ) : '';
if ( block.contexts.has( name ) ) { if ( block.contexts.has( name ) ) {
const prop = dependencies[0]; const prop = dependencies[0];
const computed = isComputed( attribute.value );
return deindent` return deindent`
var list = this.${context}.${block.listNames.get( name )}; var list = this.${context}.${block.listNames.get( name )};
var index = this.${context}.${block.indexNames.get( name )}; var index = this.${context}.${block.indexNames.get( name )};
${computed && `var state = ${block.component}.get();`}
list[index]${tail} = ${value}; list[index]${tail} = ${value};
${block.component}._set({ ${prop}: ${block.component}.get( '${prop}' ) }); ${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 ); const alias = block.alias( name );
return deindent` return deindent`
var ${alias} = ${block.component}.get( '${name}' ); var state = ${block.component}.get();
${alias}${tail} = ${value}; ${snippet} = ${value};
${block.component}._set({ ${name}: ${alias} }); ${block.component}._set({ ${name}: state.${name} });
`; `;
} }
@ -35,3 +37,12 @@ function getTailSnippet ( node ) {
return `[✂${start}-${end}✂]`; return `[✂${start}-${end}✂]`;
} }
function isComputed ( node ) {
while ( node.type === 'MemberExpression' ) {
if ( node.computed ) return true;
node = node.object;
}
return false;
}

@ -0,0 +1,55 @@
export default {
data: {
prop: 'bar',
obj: {
foo: 'a',
bar: 'b',
baz: 'c'
}
},
html: `
<input>
<pre>{"foo":"a","bar":"b","baz":"c"}</pre>
`,
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, `
<input>
<pre>{"foo":"a","bar":"e","baz":"c"}</pre>
` );
// edit baz
component.set({ prop: 'baz' });
assert.equal( input.value, 'c' );
input.value = 'f';
input.dispatchEvent( event );
assert.htmlEqual( target.innerHTML, `
<input>
<pre>{"foo":"a","bar":"e","baz":"f"}</pre>
` );
// edit foo
component.set({ prop: 'foo' });
assert.equal( input.value, 'a' );
input.value = 'd';
input.dispatchEvent( event );
assert.htmlEqual( target.innerHTML, `
<input>
<pre>{"foo":"d","bar":"e","baz":"f"}</pre>
` );
}
};

@ -0,0 +1,2 @@
<input bind:value='obj[prop]'>
<pre>{{JSON.stringify(obj)}}</pre>

@ -0,0 +1,30 @@
export default {
data: {
prop: 'name',
user: {
name: 'alice'
}
},
html: `<input>\n<p>hello alice</p>`,
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, `<input>\n<p>hello bob</p>` );
const user = component.get( 'user' );
user.name = 'carol';
component.set({ user });
assert.equal( input.value, 'carol' );
assert.equal( target.innerHTML, `<input>\n<p>hello carol</p>` );
}
};

@ -0,0 +1,2 @@
<input bind:value='user[prop]'>
<p>hello {{user.name}}</p>

@ -0,0 +1,55 @@
export default {
data: {
prop: 'bar',
objects: [{
foo: 'a',
bar: 'b',
baz: 'c'
}]
},
html: `
<input>
<pre>{"foo":"a","bar":"b","baz":"c"}</pre>
`,
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, `
<input>
<pre>{"foo":"a","bar":"e","baz":"c"}</pre>
` );
// edit baz
component.set({ prop: 'baz' });
assert.equal( input.value, 'c' );
input.value = 'f';
input.dispatchEvent( event );
assert.htmlEqual( target.innerHTML, `
<input>
<pre>{"foo":"a","bar":"e","baz":"f"}</pre>
` );
// edit foo
component.set({ prop: 'foo' });
assert.equal( input.value, 'a' );
input.value = 'd';
input.dispatchEvent( event );
assert.htmlEqual( target.innerHTML, `
<input>
<pre>{"foo":"d","bar":"e","baz":"f"}</pre>
` );
}
};

@ -0,0 +1,4 @@
{{#each objects as obj}}
<input bind:value='obj[prop]'>
<pre>{{JSON.stringify(obj)}}</pre>
{{/each}}
Loading…
Cancel
Save