Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support binding to computed member expressions #605

Merged
merged 2 commits into from
Jun 1, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/generators/dom/visitors/Component/Binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
20 changes: 15 additions & 5 deletions src/generators/dom/visitors/Element/Binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!' );

Expand All @@ -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}`;
Expand Down Expand Up @@ -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 );
Expand Down
19 changes: 15 additions & 4 deletions src/generators/dom/visitors/shared/binding/getSetter.ts
Original file line number Diff line number Diff line change
@@ -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}' ) });
Expand All @@ -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} });
`;
}

Expand All @@ -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;
}
2 changes: 1 addition & 1 deletion src/parse/read/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export function readBindingDirective ( parser: Parser, start: number, name: stri
value = parseExpressionAt( source, a );

if ( value.type !== 'Identifier' && value.type !== 'MemberExpression' ) {
parser.error( `Expected valid property name` );
parser.error( `Cannot bind to rvalue`, value.start );
}

parser.allowWhitespace();
Expand Down
8 changes: 8 additions & 0 deletions test/parser/samples/error-binding-rvalue/error.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"message": "Cannot bind to rvalue",
"pos": 19,
"loc": {
"line": 1,
"column": 19
}
}
1 change: 1 addition & 0 deletions test/parser/samples/error-binding-rvalue/input.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<input bind:value='a + b'>
Original file line number Diff line number Diff line change
@@ -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>
` );
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<input bind:value='obj[prop]'>
<pre>{{JSON.stringify(obj)}}</pre>
30 changes: 30 additions & 0 deletions test/runtime/samples/binding-input-text-deep-computed/_config.js
Original file line number Diff line number Diff line change
@@ -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>` );
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<input bind:value='user[prop]'>
<p>hello {{user.name}}</p>
Original file line number Diff line number Diff line change
@@ -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>
` );
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{{#each objects as obj}}
<input bind:value='obj[prop]'>
<pre>{{JSON.stringify(obj)}}</pre>
{{/each}}