Merge branch 'master' into gh-312

pull/386/head
Rich Harris 8 years ago
commit 0f9ef05afa

@ -1,4 +1,4 @@
import createBinding from './binding/index.js';
import addComponentBinding from './addComponentBinding.js';
import deindent from '../../../../utils/deindent.js';
export default function addComponentAttributes ( generator, node, local ) {
@ -112,7 +112,7 @@ export default function addComponentAttributes ( generator, node, local ) {
}
else if ( attribute.type === 'Binding' ) {
createBinding( generator, node, attribute, generator.current, local );
addComponentBinding( generator, node, attribute, generator.current, local );
}
else if ( attribute.type === 'Ref' ) {

@ -0,0 +1,60 @@
import deindent from '../../../../utils/deindent.js';
import flattenReference from '../../../../utils/flattenReference.js';
import getSetter from './binding/getSetter.js';
export default function createBinding ( generator, node, attribute, current, local ) {
const { name } = flattenReference( attribute.value );
const { snippet, contexts, dependencies } = generator.contextualise( attribute.value );
if ( dependencies.length > 1 ) throw new Error( 'An unexpected situation arose. Please raise an issue at https://github.com/sveltejs/svelte/issues — thanks!' );
contexts.forEach( context => {
if ( !~local.allUsedContexts.indexOf( context ) ) local.allUsedContexts.push( context );
});
const contextual = name in current.contexts;
let obj;
let prop;
if ( contextual ) {
obj = current.listNames[ name ];
prop = current.indexNames[ name ];
} else if ( attribute.value.type === 'MemberExpression' ) {
prop = `'[✂${attribute.value.property.start}-${attribute.value.property.end}✂]}'`;
obj = `root.[✂${attribute.value.object.start}-${attribute.value.object.end}✂]}`;
} else {
obj = 'root';
prop = `'${name}'`;
}
local.bindings.push({
name: attribute.name,
value: snippet,
obj,
prop
});
const setter = getSetter({ current, name, context: '_context', attribute, dependencies, snippet, value: 'value' });
generator.hasComplexBindings = true;
local.init.addBlock( deindent`
var ${local.name}_updating = false;
component._bindings.push( function () {
if ( ${local.name}._torndown ) return;
${local.name}.observe( '${attribute.name}', function ( value ) {
${local.name}_updating = true;
${setter}
${local.name}_updating = false;
});
});
` );
local.update.addBlock( deindent`
if ( !${local.name}_updating && ${dependencies.map( dependency => `'${dependency}' in changed` ).join( '||' )} ) {
${local.name}._set({ ${attribute.name}: ${snippet} });
}
` );
}

@ -1,5 +1,5 @@
import attributeLookup from './lookup.js';
import createBinding from './binding/index.js';
import addElementBinding from './addElementBinding';
import deindent from '../../../../utils/deindent.js';
import flattenReference from '../../../../utils/flattenReference.js';
@ -204,7 +204,7 @@ export default function addElementAttributes ( generator, node, local ) {
}
else if ( attribute.type === 'Binding' ) {
createBinding( generator, node, attribute, generator.current, local );
addElementBinding( generator, node, attribute, generator.current, local );
}
else if ( attribute.type === 'Ref' ) {

@ -0,0 +1,106 @@
import deindent from '../../../../utils/deindent.js';
import flattenReference from '../../../../utils/flattenReference.js';
import getSetter from './binding/getSetter.js';
export default function createBinding ( generator, node, attribute, current, local ) {
const { name } = flattenReference( attribute.value );
const { snippet, contexts, dependencies } = generator.contextualise( attribute.value );
if ( dependencies.length > 1 ) throw new Error( 'An unexpected situation arose. Please raise an issue at https://github.com/sveltejs/svelte/issues — thanks!' );
contexts.forEach( context => {
if ( !~local.allUsedContexts.indexOf( context ) ) local.allUsedContexts.push( context );
});
const handler = current.getUniqueName( `${local.name}ChangeHandler` );
const isMultipleSelect = node.name === 'select' && node.attributes.find( attr => attr.name.toLowerCase() === 'multiple' ); // TODO ensure that this is a static attribute
const value = getBindingValue( local, node, attribute, isMultipleSelect );
const eventName = getBindingEventName( node );
let setter = getSetter({ current, name, context: '__svelte', attribute, dependencies, snippet, value });
// special case
if ( node.name === 'select' && !isMultipleSelect ) {
setter = `var selectedOption = ${local.name}.selectedOptions[0] || ${local.name}.options[0];\n` + setter;
}
let updateElement;
if ( node.name === 'select' ) {
const value = generator.current.getUniqueName( 'value' );
const i = generator.current.getUniqueName( 'i' );
const option = generator.current.getUniqueName( 'option' );
const ifStatement = isMultipleSelect ?
deindent`
${option}.selected = ~${value}.indexOf( ${option}.__value );` :
deindent`
if ( ${option}.__value === ${value} ) {
${option}.selected = true;
break;
}`;
updateElement = deindent`
var ${value} = ${snippet};
for ( var ${i} = 0; ${i} < ${local.name}.options.length; ${i} += 1 ) {
var ${option} = ${local.name}.options[${i}];
${ifStatement}
}
`;
} else {
updateElement = `${local.name}.${attribute.name} = ${snippet};`;
}
local.init.addBlock( deindent`
var ${local.name}_updating = false;
function ${handler} () {
${local.name}_updating = true;
${setter}
${local.name}_updating = false;
}
${generator.helper( 'addEventListener' )}( ${local.name}, '${eventName}', ${handler} );
` );
node.initialUpdate = updateElement;
local.update.addLine( deindent`
if ( !${local.name}_updating ) {
${updateElement}
}
` );
generator.current.builders.teardown.addLine( deindent`
${generator.helper( 'removeEventListener' )}( ${local.name}, '${eventName}', ${handler} );
` );
}
function getBindingEventName ( node ) {
if ( node.name === 'input' ) {
const typeAttribute = node.attributes.find( attr => attr.type === 'Attribute' && attr.name === 'type' );
const type = typeAttribute ? typeAttribute.value[0].data : 'text'; // TODO in validation, should throw if type attribute is not static
return type === 'checkbox' || type === 'radio' ? 'change' : 'input';
}
if ( node.name === 'textarea' ) {
return 'input';
}
return 'change';
}
function getBindingValue ( local, node, attribute, isMultipleSelect ) {
if ( isMultipleSelect ) {
return `[].map.call( ${local.name}.selectedOptions, function ( option ) { return option.__value; })`;
}
if ( node.name === 'select' ) {
return 'selectedOption && selectedOption.__value';
}
return `${local.name}.${attribute.name}`;
}

@ -0,0 +1,34 @@
import deindent from '../../../../../utils/deindent.js';
export default function getSetter ({ current, name, context, attribute, dependencies, snippet, value }) {
if ( name in current.contexts ) {
const prop = dependencies[0];
const tail = attribute.value.type === 'MemberExpression' ? getTailSnippet( attribute.value ) : '';
return deindent`
var list = this.${context}.${current.listNames[ name ]};
var index = this.${context}.${current.indexNames[ name ]};
list[index]${tail} = ${value};
component._set({ ${prop}: component.get( '${prop}' ) });
`;
}
if ( attribute.value.type === 'MemberExpression' ) {
return deindent`
var ${name} = component.get( '${name}' );
${snippet} = ${value};
component._set({ ${name}: ${name} });
`;
}
return `component._set({ ${name}: ${value} });`;
}
function getTailSnippet ( node ) {
const end = node.end;
while ( node.type === 'MemberExpression' ) node = node.object;
const start = node.end;
return `[✂${start}-${end}✂]`;
}

@ -1,190 +0,0 @@
import deindent from '../../../../../utils/deindent.js';
import isReference from '../../../../../utils/isReference.js';
import flattenReference from '../../../../../utils/flattenReference.js';
export default function createBinding ( generator, node, attribute, current, local ) {
const parts = attribute.value.split( '.' );
const deep = parts.length > 1;
const contextual = parts[0] in current.contexts;
if ( contextual && !~local.allUsedContexts.indexOf( parts[0] ) ) {
local.allUsedContexts.push( parts[0] );
}
if ( local.isComponent ) {
let obj;
let prop;
let value;
if ( contextual ) {
obj = current.listNames[ parts[0] ];
prop = current.indexNames[ parts[0] ];
value = attribute.value;
} else {
prop = `'${parts.slice( -1 )}'`;
obj = parts.length > 1 ? `root.${parts.slice( 0, -1 ).join( '.' )}` : `root`;
value = `root.${attribute.value}`;
}
local.bindings.push({ name: attribute.name, value, obj, prop });
}
const handler = current.getUniqueName( `${local.name}ChangeHandler` );
let setter;
let eventName = 'change';
if ( node.name === 'input' ) {
const typeAttribute = node.attributes.find( attr => attr.type === 'Attribute' && attr.name === 'type' );
const type = typeAttribute ? typeAttribute.value[0].data : 'text'; // TODO in validation, should throw if type attribute is not static
if ( type !== 'checkbox' && type !== 'radio' ) {
eventName = 'input';
}
}
else if ( node.name === 'textarea' ) {
eventName = 'input';
}
const isMultipleSelect = node.name === 'select' && node.attributes.find( attr => attr.name.toLowerCase() === 'multiple' ); // TODO ensure that this is a static attribute
let value;
if ( local.isComponent ) {
value = 'value';
} else if ( node.name === 'select' ) {
if ( isMultipleSelect ) {
value = `[].map.call( ${local.name}.selectedOptions, function ( option ) { return option.__value; })`;
} else {
value = 'selectedOption && selectedOption.__value';
}
} else {
value = `${local.name}.${attribute.name}`;
}
if ( contextual ) {
// find the top-level property that this is a child of
let fragment = current;
let prop = parts[0];
do {
if ( fragment.expression && fragment.context === prop ) {
if ( !isReference( fragment.expression ) ) {
// TODO this should happen in prior validation step
throw new Error( `${prop} is read-only, it cannot be bound` );
}
prop = flattenReference( fragment.expression ).name;
}
} while ( fragment = fragment.parent );
generator.expectedProperties[ prop ] = true;
const listName = current.listNames[ parts[0] ];
const indexName = current.indexNames[ parts[0] ];
const context = local.isComponent ? `_context` : `__svelte`;
setter = deindent`
var list = this.${context}.${listName};
var index = this.${context}.${indexName};
list[index]${parts.slice( 1 ).map( part => `.${part}` ).join( '' )} = ${value};
component._set({ ${prop}: component.get( '${prop}' ) });
`;
} else {
if ( deep ) {
setter = deindent`
var ${parts[0]} = component.get( '${parts[0]}' );
${parts[0]}.${parts.slice( 1 ).join( '.' )} = ${value};
component._set({ ${parts[0]}: ${parts[0]} });
`;
} else {
setter = `component._set({ ${attribute.value}: ${value} });`;
}
generator.expectedProperties[ parts[0] ] = true;
}
// special case
if ( node.name === 'select' && !isMultipleSelect ) {
setter = `var selectedOption = ${local.name}.selectedOptions[0] || ${local.name}.options[0];\n` + setter;
}
if ( local.isComponent ) {
generator.hasComplexBindings = true;
local.init.addBlock( deindent`
var ${local.name}_updating = false;
component._bindings.push( function () {
if ( ${local.name}._torndown ) return;
${local.name}.observe( '${attribute.name}', function ( value ) {
${local.name}_updating = true;
${setter}
${local.name}_updating = false;
});
});
` );
const dependencies = parts[0] in current.contexts ? current.contextDependencies[ parts[0] ] : [ parts[0] ];
local.update.addBlock( deindent`
if ( !${local.name}_updating && ${dependencies.map( dependency => `'${dependency}' in changed` ).join( '||' )} ) {
${local.name}._set({ ${attribute.name}: ${contextual ? attribute.value : `root.${attribute.value}`} });
}
` );
} else {
let updateElement;
if ( node.name === 'select' ) {
const value = generator.current.getUniqueName( 'value' );
const i = generator.current.getUniqueName( 'i' );
const option = generator.current.getUniqueName( 'option' );
const ifStatement = isMultipleSelect ?
deindent`
${option}.selected = ~${value}.indexOf( ${option}.__value );` :
deindent`
if ( ${option}.__value === ${value} ) {
${option}.selected = true;
break;
}`;
updateElement = deindent`
var ${value} = ${contextual ? attribute.value : `root.${attribute.value}`};
for ( var ${i} = 0; ${i} < ${local.name}.options.length; ${i} += 1 ) {
var ${option} = ${local.name}.options[${i}];
${ifStatement}
}
`;
} else {
updateElement = `${local.name}.${attribute.name} = ${contextual ? attribute.value : `root.${attribute.value}`};`;
}
local.init.addBlock( deindent`
var ${local.name}_updating = false;
function ${handler} () {
${local.name}_updating = true;
${setter}
${local.name}_updating = false;
}
${generator.helper( 'addEventListener' )}( ${local.name}, '${eventName}', ${handler} );
` );
node.initialUpdate = updateElement;
local.update.addLine( deindent`
if ( !${local.name}_updating ) {
${updateElement}
}
` );
generator.current.builders.teardown.addLine( deindent`
${generator.helper( 'removeEventListener' )}( ${local.name}, '${eventName}', ${handler} );
` );
}
}

@ -1,5 +1,6 @@
import deindent from '../../utils/deindent.js';
import CodeBuilder from '../../utils/CodeBuilder.js';
import flattenReference from '../../utils/flattenReference.js';
import processCss from '../shared/processCss.js';
import visitors from './visitors/index.js';
import Generator from '../Generator.js';
@ -16,11 +17,13 @@ class SsrGenerator extends Generator {
this.current.conditions.map( c => `(${c})` )
);
const { keypath } = flattenReference( binding.value );
this.bindings.push( deindent`
if ( ${conditions.join( '&&' )} ) {
tmp = ${name}.data();
if ( '${binding.value}' in tmp ) {
root.${binding.name} = tmp.${binding.value};
if ( '${keypath}' in tmp ) {
root.${binding.name} = tmp.${keypath};
settled = false;
}
}

@ -1,3 +1,5 @@
import flattenReference from '../../../utils/flattenReference.js';
export default {
enter ( generator, node ) {
function stringify ( chunk ) {
@ -42,8 +44,8 @@ export default {
return `${attribute.name}: ${value}`;
})
.concat( bindings.map( binding => {
const parts = binding.value.split( '.' );
const value = parts[0] in generator.current.contexts ? binding.value : `root.${binding.value}`;
const { name, keypath } = flattenReference( binding.value );
const value = name in generator.current.contexts ? keypath : `root.${keypath}`;
return `${binding.name}: ${value}`;
}))
.join( ', ' );

@ -1,4 +1,4 @@
import { parse } from 'acorn';
import { parse, parseExpressionAt } from 'acorn';
import spaces from '../../utils/spaces.js';
export function readEventHandlerDirective ( parser, start, name ) {
@ -66,7 +66,7 @@ export function readEventHandlerDirective ( parser, start, name ) {
}
export function readBindingDirective ( parser, start, name ) {
let value = name; // shorthand bind:foo equivalent to bind:foo='foo'
let value;
if ( parser.eat( '=' ) ) {
const quoteMark = (
@ -75,12 +75,37 @@ export function readBindingDirective ( parser, start, name ) {
null
);
value = parser.read( /([a-zA-Z_$][a-zA-Z0-9_$]*)(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*/ );
if ( !value ) parser.error( `Expected valid property name` );
const a = parser.index;
// this is a bit of a hack so that we can give Acorn something parseable
let b;
if ( quoteMark ) {
b = parser.index = parser.template.indexOf( quoteMark, parser.index );
} else {
parser.readUntil( /[\s\r\n\/>]/ );
b = parser.index;
}
const source = spaces( a ) + parser.template.slice( a, b );
value = parseExpressionAt( source, a );
if ( value.type !== 'Identifier' && value.type !== 'MemberExpression' ) {
parser.error( `Expected valid property name` );
}
parser.allowWhitespace();
if ( quoteMark ) {
parser.eat( quoteMark, true );
}
} else {
// shorthand bind:foo equivalent to bind:foo='foo'
value = {
type: 'Identifier',
start: start + 5,
end: parser.index,
name
};
}
return {

@ -12,5 +12,5 @@ export default function flatten ( node ) {
if ( !name ) return null;
parts.unshift( name );
return { name, keypath: parts.join( '.' ) };
return { name, parts, keypath: parts.join( '.' ) };
}

@ -2,7 +2,9 @@ export default {
data: {
foo: true
},
html: `<input type="checkbox">\n<p>true</p>`,
test ( assert, component, target, window ) {
const input = target.querySelector( 'input' );
assert.equal( input.checked, true );

@ -15,7 +15,12 @@
"end": 16,
"type": "Binding",
"name": "foo",
"value": "foo"
"value": {
"type": "Identifier",
"start": 13,
"end": 16,
"name": "foo"
}
}
],
"children": []

@ -15,7 +15,12 @@
"end": 24,
"type": "Binding",
"name": "value",
"value": "name"
"value": {
"start": 19,
"end": 23,
"type": "Identifier",
"name": "name"
}
}
],
"children": []

@ -0,0 +1,11 @@
<Widget bind:potato/>
<script>
import Widget from 'wherever';
export default {
components: {
Widget
}
};
</script>

@ -0,0 +1,21 @@
export function test ({ assert, smc, locateInSource, locateInGenerated }) {
const expected = locateInSource( 'potato' );
let loc;
loc = locateInGenerated( 'potato' );
loc = locateInGenerated( 'potato', loc.character + 1 );
loc = locateInGenerated( 'potato', loc.character + 1 ); // we need the third instance of 'potato'
const actual = smc.originalPositionFor({
line: loc.line + 1,
column: loc.column
});
assert.deepEqual( actual, {
source: 'input.html',
name: null,
line: expected.line + 1,
column: expected.column
});
}

@ -0,0 +1 @@
<input bind:value='foo.bar.baz'>

@ -0,0 +1,34 @@
export function test ({ assert, smc, locateInSource, locateInGenerated }) {
const expected = locateInSource( 'foo.bar.baz' );
let loc;
let actual;
loc = locateInGenerated( 'foo.bar.baz' );
actual = smc.originalPositionFor({
line: loc.line + 1,
column: loc.column
});
assert.deepEqual( actual, {
source: 'input.html',
name: null,
line: expected.line + 1,
column: expected.column
});
loc = locateInGenerated( 'foo.bar.baz', loc.character + 1 );
actual = smc.originalPositionFor({
line: loc.line + 1,
column: loc.column
});
assert.deepEqual( actual, {
source: 'input.html',
name: null,
line: expected.line + 1,
column: expected.column
});
}
Loading…
Cancel
Save