mirror of https://github.com/sveltejs/svelte
Merge pull request #380 from sveltejs/unified-attributes-and-bindings
[WIP] Unified attributes and bindingspull/384/head
commit
ecf141346e
@ -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} });
|
||||||
|
}
|
||||||
|
` );
|
||||||
|
}
|
@ -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} );
|
|
||||||
` );
|
|
||||||
}
|
|
||||||
}
|
|
@ -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…
Reference in new issue