Merge pull request #456 from sveltejs/gh-433-b

Avoid binding event handler callbacks, version 2
pull/457/head
Rich Harris 8 years ago committed by GitHub
commit 20298b1a0a

@ -60,27 +60,34 @@ export default class Generator {
return alias;
}
contextualise ( fragment, expression, isEventHandler ) {
contextualise ( block, expression, context, isEventHandler ) {
this.addSourcemapLocations( expression );
const usedContexts = [];
const dependencies = [];
const { code, helpers } = this;
const { contextDependencies, contexts, indexes } = fragment;
const { contextDependencies, contexts, indexes } = block;
let scope = annotateWithScopes( expression );
let lexicalDepth = 0;
const self = this;
walk( expression, {
enter ( node, parent, key ) {
if ( /^Function/.test( node.type ) ) lexicalDepth += 1;
if ( node._scope ) {
scope = node._scope;
return;
}
if ( isReference( node, parent ) ) {
if ( node.type === 'ThisExpression' ) {
if ( lexicalDepth === 0 && context ) code.overwrite( node.start, node.end, context, true );
}
else if ( isReference( node, parent ) ) {
const { name } = flattenReference( node );
if ( scope.has( name ) ) return;
@ -93,10 +100,10 @@ export default class Generator {
}
else if ( contexts.has( name ) ) {
const context = contexts.get( name );
if ( context !== name ) {
const contextName = contexts.get( name );
if ( contextName !== name ) {
// this is true for 'reserved' names like `root` and `component`
code.overwrite( node.start, node.start + name.length, context, true );
code.overwrite( node.start, node.start + name.length, contextName, true );
}
dependencies.push( ...contextDependencies.get( name ) );
@ -133,6 +140,7 @@ export default class Generator {
},
leave ( node ) {
if ( /^Function/.test( node.type ) ) lexicalDepth -= 1;
if ( node._scope ) scope = scope.parent;
}
});

@ -39,7 +39,7 @@ export default class Block {
`var ${name} = ${renderStatement};`
);
this.createMountStatement( name, parentNode );
this.mount( name, parentNode );
} else {
this.builders.create.addLine( `${this.generator.helper( 'appendNode' )}( ${renderStatement}, ${parentNode} );` );
}
@ -53,12 +53,16 @@ export default class Block {
return new Block( Object.assign( {}, this, options, { parent: this } ) );
}
contextualise ( expression, context, isEventHandler ) {
return this.generator.contextualise( this, expression, context, isEventHandler );
}
createAnchor ( name, parentNode ) {
const renderStatement = `${this.generator.helper( 'createComment' )}()`;
this.addElement( name, renderStatement, parentNode, true );
}
createMountStatement ( name, parentNode ) {
mount ( name, parentNode ) {
if ( parentNode ) {
this.builders.create.addLine( `${this.generator.helper( 'appendNode' )}( ${name}, ${parentNode} );` );
} else {

@ -271,7 +271,7 @@ export default function dom ( parsed, source, options ) {
throw new Error( `Components with shared helpers must be compiled to ES2015 modules (format: 'es')` );
}
const names = Array.from( generator.uses ).map( name => {
const names = Array.from( generator.uses ).sort().map( name => {
return name !== generator.alias( name ) ? `${name} as ${generator.alias( name )}` : name;
});

@ -0,0 +1,67 @@
export default function visitAttribute ( generator, block, state, node, attribute, local ) {
if ( attribute.value === true ) {
// attributes without values, e.g. <textarea readonly>
local.staticAttributes.push({
name: attribute.name,
value: true
});
}
else if ( attribute.value.length === 0 ) {
local.staticAttributes.push({
name: attribute.name,
value: `''`
});
}
else if ( attribute.value.length === 1 ) {
const value = attribute.value[0];
if ( value.type === 'Text' ) {
// static attributes
const result = isNaN( value.data ) ? JSON.stringify( value.data ) : value.data;
local.staticAttributes.push({
name: attribute.name,
value: result
});
}
else {
// simple dynamic attributes
const { dependencies, string } = generator.contextualise( block, value.expression );
// TODO only update attributes that have changed
local.dynamicAttributes.push({
name: attribute.name,
value: string,
dependencies
});
}
}
else {
// complex dynamic attributes
const allDependencies = [];
const value = ( attribute.value[0].type === 'Text' ? '' : `"" + ` ) + (
attribute.value.map( chunk => {
if ( chunk.type === 'Text' ) {
return JSON.stringify( chunk.data );
} else {
const { dependencies, string } = generator.contextualise( block, chunk.expression );
dependencies.forEach( dependency => {
if ( !~allDependencies.indexOf( dependency ) ) allDependencies.push( dependency );
});
return `( ${string} )`;
}
}).join( ' + ' )
);
local.dynamicAttributes.push({
name: attribute.name,
value,
dependencies: allDependencies
});
}
}

@ -1,8 +1,8 @@
import deindent from '../../../../utils/deindent.js';
import flattenReference from '../../../../utils/flattenReference.js';
import getSetter from './binding/getSetter.js';
import getSetter from '../shared/binding/getSetter.js';
export default function addComponentBinding ( generator, node, attribute, block, local ) {
export default function visitBinding ( generator, block, state, node, attribute, local ) {
const { name, keypath } = flattenReference( attribute.value );
const { snippet, contexts, dependencies } = generator.contextualise( block, attribute.value );
@ -62,4 +62,4 @@ export default function addComponentBinding ( generator, node, attribute, block,
${updating} = false;
}
` );
}
}

@ -1,7 +1,10 @@
import deindent from '../../../utils/deindent.js';
import CodeBuilder from '../../../utils/CodeBuilder.js';
import visit from '../visit.js';
import addComponentAttributes from './attributes/addComponentAttributes.js';
import deindent from '../../../../utils/deindent.js';
import CodeBuilder from '../../../../utils/CodeBuilder.js';
import visit from '../../visit.js';
import visitAttribute from './Attribute.js';
import visitEventHandler from './EventHandler.js';
import visitBinding from './Binding.js';
import visitRef from './Ref.js';
function capDown ( name ) {
return `${name[0].toLowerCase()}${name.slice( 1 )}`;
@ -19,16 +22,37 @@ function stringifyProps ( props ) {
return `{ ${joined} }`;
}
const order = {
Attribute: 1,
EventHandler: 2,
Binding: 3,
Ref: 4
};
const visitors = {
Attribute: visitAttribute,
EventHandler: visitEventHandler,
Binding: visitBinding,
Ref: visitRef
};
export default function visitComponent ( generator, block, state, node ) {
const hasChildren = node.children.length > 0;
const name = block.getUniqueName( capDown( node.name === ':Self' ? generator.name : node.name ) );
const childState = Object.assign( {}, state, {
parentNode: null
});
const local = {
name,
namespace: state.namespace,
isComponent: true,
allUsedContexts: [],
staticAttributes: [],
dynamicAttributes: [],
bindings: [],
create: new CodeBuilder(),
update: new CodeBuilder()
@ -38,7 +62,11 @@ export default function visitComponent ( generator, block, state, node ) {
generator.hasComponents = true;
addComponentAttributes( generator, block, node, local );
node.attributes
.sort( ( a, b ) => order[ a.type ] - order[ b.type ] )
.forEach( attribute => {
visitors[ attribute.type ]( generator, block, childState, node, attribute, local );
});
if ( local.allUsedContexts.length ) {
const initialProps = local.allUsedContexts.map( contextName => {
@ -81,10 +109,6 @@ export default function visitComponent ( generator, block, state, node ) {
name: generator.getUniqueName( `create_${name}_yield_fragment` ) // TODO should getUniqueName happen inside Fragment? probably
});
const childState = Object.assign( {}, state, {
parentNode: null
});
node.children.forEach( child => {
visit( generator, childBlock, childState, child );
});

@ -0,0 +1,35 @@
import deindent from '../../../../utils/deindent.js';
export default function visitEventHandler ( generator, block, state, node, attribute, local ) {
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.addSourcemapLocations( attribute.expression );
generator.code.prependRight( attribute.expression.start, `${block.component}.` );
const usedContexts = [];
attribute.expression.arguments.forEach( arg => {
const { contexts } = generator.contextualise( block, arg, null, true );
contexts.forEach( context => {
if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context );
if ( !~local.allUsedContexts.indexOf( context ) ) local.allUsedContexts.push( context );
});
});
// TODO hoist event handlers? can do `this.__component.method(...)`
const declarations = usedContexts.map( name => {
if ( name === 'root' ) return 'var root = this._context.root;';
const listName = block.listNames.get( name );
const indexName = block.indexNames.get( name );
return `var ${listName} = this._context.${listName}, ${indexName} = this._context.${indexName}, ${name} = ${listName}[${indexName}]`;
});
const handlerBody = ( declarations.length ? declarations.join( '\n' ) + '\n\n' : '' ) + `[✂${attribute.expression.start}-${attribute.expression.end}✂];`;
local.create.addBlock( deindent`
${local.name}.on( '${attribute.name}', function ( event ) {
${handlerBody}
});
` );
}

@ -0,0 +1,13 @@
import deindent from '../../../../utils/deindent.js';
export default function visitRef ( generator, block, state, node, attribute, local ) {
generator.usesRefs = true;
local.create.addLine(
`${block.component}.refs.${attribute.name} = ${local.name};`
);
block.builders.destroy.addLine( deindent`
if ( ${block.component}.refs.${attribute.name} === ${local.name} ) ${block.component}.refs.${attribute.name} = null;
` );
}

@ -134,7 +134,7 @@ export default function visitEachBlock ( generator, block, state, node ) {
}
}
destroyEach( ${localVars.iterations}, true, ${listName}.length );
${generator.helper( 'destroyEach' )}( ${localVars.iterations}, true, ${listName}.length );
${localVars.iterations}.length = ${listName}.length;
` );
@ -154,7 +154,7 @@ export default function visitEachBlock ( generator, block, state, node ) {
}
block.builders.destroy.addBlock(
`${generator.helper( 'destroyEach' )}( ${localVars.iterations}, ${isToplevel ? 'detach' : 'false'} );` );
`${generator.helper( 'destroyEach' )}( ${localVars.iterations}, ${isToplevel ? 'detach' : 'false'}, 0 );` );
if ( node.else ) {
block.builders.destroy.addBlock( deindent`
@ -197,7 +197,8 @@ export default function visitEachBlock ( generator, block, state, node ) {
});
const childState = Object.assign( {}, state, {
parentNode: null
parentNode: null,
inEachBlock: true
});
node.children.forEach( child => {

@ -1,114 +0,0 @@
import CodeBuilder from '../../../utils/CodeBuilder.js';
import deindent from '../../../utils/deindent.js';
import visit from '../visit.js';
import addElementAttributes from './attributes/addElementAttributes.js';
import visitComponent from './Component.js';
import visitWindow from './meta/Window.js';
const meta = {
':Window': visitWindow
};
export default function visitElement ( generator, block, state, node ) {
if ( node.name in meta ) {
return meta[ node.name ]( generator, block, node );
}
if ( generator.components.has( node.name ) || node.name === ':Self' ) {
return visitComponent( generator, block, state, node );
}
const name = block.getUniqueName( node.name );
const local = {
name,
namespace: node.name === 'svg' ? 'http://www.w3.org/2000/svg' : state.namespace,
isComponent: false,
allUsedContexts: [],
create: new CodeBuilder(),
update: new CodeBuilder(),
destroy: new CodeBuilder()
};
const isToplevel = !state.parentNode;
addElementAttributes( generator, block, node, local );
if ( local.allUsedContexts.length ) {
const initialProps = local.allUsedContexts.map( contextName => {
if ( contextName === 'root' ) return `root: root`;
const listName = block.listNames.get( contextName );
const indexName = block.indexNames.get( contextName );
return `${listName}: ${listName},\n${indexName}: ${indexName}`;
}).join( ',\n' );
const updates = local.allUsedContexts.map( contextName => {
if ( contextName === 'root' ) return `${name}.__svelte.root = root;`;
const listName = block.listNames.get( contextName );
const indexName = block.indexNames.get( contextName );
return `${name}.__svelte.${listName} = ${listName};\n${name}.__svelte.${indexName} = ${indexName};`;
}).join( '\n' );
local.create.addBlock( deindent`
${name}.__svelte = {
${initialProps}
};
` );
local.update.addBlock( updates );
}
let render;
if ( local.namespace ) {
if ( local.namespace === 'http://www.w3.org/2000/svg' ) {
render = `var ${name} = ${generator.helper( 'createSvgElement' )}( '${node.name}' )`;
} else {
render = `var ${name} = document.createElementNS( '${local.namespace}', '${node.name}' );`;
}
} else {
render = `var ${name} = ${generator.helper( 'createElement' )}( '${node.name}' );`;
}
if ( generator.cssId && state.isTopLevel ) {
render += `\n${generator.helper( 'setAttribute' )}( ${name}, '${generator.cssId}', '' );`;
}
local.create.addLineAtStart( render );
if ( isToplevel ) {
block.builders.detach.addLine( `${generator.helper( 'detachNode' )}( ${name} );` );
}
// special case bound <option> without a value attribute
if ( node.name === 'option' && !node.attributes.find( attribute => attribute.type === 'Attribute' && attribute.name === 'value' ) ) { // TODO check it's bound
const statement = `${name}.__value = ${name}.textContent;`;
local.update.addLine( statement );
node.initialUpdate = statement;
}
block.builders.create.addBlock( local.create );
if ( !local.update.isEmpty() ) block.builders.update.addBlock( local.update );
if ( !local.destroy.isEmpty() ) block.builders.destroy.addBlock( local.destroy );
block.createMountStatement( name, state.parentNode );
const childState = Object.assign( {}, state, {
isTopLevel: false,
parentNode: name,
namespace: local.namespace
});
node.children.forEach( child => {
visit( generator, block, childState, child );
});
if ( node.initialUpdate ) {
block.builders.create.addBlock( node.initialUpdate );
}
}

@ -0,0 +1,92 @@
import attributeLookup from './lookup.js';
import deindent from '../../../../utils/deindent.js';
import getStaticAttributeValue from './getStaticAttributeValue.js';
export default function visitAttribute ( generator, block, state, node, attribute ) {
const name = attribute.name;
let metadata = state.namespace ? null : attributeLookup[ name ];
if ( metadata && metadata.appliesTo && !~metadata.appliesTo.indexOf( node.name ) ) metadata = null;
const isIndirectlyBoundValue = name === 'value' && (
node.name === 'option' || // TODO check it's actually bound
node.name === 'input' && /^(checkbox|radio)$/.test( getStaticAttributeValue( node, 'type' ) )
);
const propertyName = isIndirectlyBoundValue ? '__value' : metadata && metadata.propertyName;
// xlink is a special case... we could maybe extend this to generic
// namespaced attributes but I'm not sure that's applicable in
// HTML5?
const method = name.slice( 0, 6 ) === 'xlink:' ? 'setXlinkAttribute' : 'setAttribute';
const isDynamic = attribute.value !== true && attribute.value.length > 1 || ( attribute.value.length === 1 && attribute.value[0].type !== 'Text' );
if ( isDynamic ) {
let value;
if ( attribute.value.length === 1 ) {
// single {{tag}} — may be a non-string
const { snippet } = generator.contextualise( block, attribute.value[0].expression );
value = snippet;
} else {
// '{{foo}} {{bar}}' — treat as string concatenation
value = ( attribute.value[0].type === 'Text' ? '' : `"" + ` ) + (
attribute.value.map( chunk => {
if ( chunk.type === 'Text' ) {
return JSON.stringify( chunk.data );
} else {
const { snippet } = generator.contextualise( block, chunk.expression );
return `( ${snippet} )`;
}
}).join( ' + ' )
);
}
const last = `last_${state.parentNode}_${name.replace( /-/g, '_')}`;
block.builders.create.addLine( `var ${last} = ${value};` );
const updater = propertyName ?
`${state.parentNode}.${propertyName} = ${last};` :
`${generator.helper( method )}( ${state.parentNode}, '${name}', ${last} );`;
block.builders.create.addLine( updater );
block.builders.update.addBlock( deindent`
if ( ( ${block.tmp()} = ${value} ) !== ${last} ) {
${last} = ${block.tmp()};
${updater}
}
` );
}
else {
const value = attribute.value === true ? 'true' :
attribute.value.length === 0 ? `''` :
JSON.stringify( attribute.value[0].data );
const statement = propertyName ?
`${state.parentNode}.${propertyName} = ${value};` :
`${generator.helper( method )}( ${state.parentNode}, '${name}', ${value} );`;
block.builders.create.addLine( statement );
// special case autofocus. has to be handled in a bit of a weird way
if ( attribute.value === true && name === 'autofocus' ) {
block.autofocus = state.parentNode;
}
// special case — xmlns
if ( name === 'xmlns' ) {
// TODO this attribute must be static enforce at compile time
state.namespace = attribute.value[0].data;
}
}
if ( isIndirectlyBoundValue ) {
const updateValue = `${state.parentNode}.value = ${state.parentNode}.__value;`;
block.builders.create.addLine( updateValue );
if ( isDynamic ) block.builders.update.addLine( updateValue );
}
}

@ -1,33 +1,33 @@
import deindent from '../../../../utils/deindent.js';
import flattenReference from '../../../../utils/flattenReference.js';
import getSetter from './binding/getSetter.js';
import getStaticAttributeValue from './binding/getStaticAttributeValue.js';
import getSetter from '../shared/binding/getSetter.js';
import getStaticAttributeValue from './getStaticAttributeValue.js';
export default function addElementBinding ( generator, node, attribute, block, local ) {
export default function visitBinding ( generator, block, state, node, attribute ) {
const { name, keypath } = flattenReference( attribute.value );
const { snippet, contexts, dependencies } = generator.contextualise( block, 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 );
if ( !~state.allUsedContexts.indexOf( context ) ) state.allUsedContexts.push( context );
});
const handler = block.getUniqueName( `${local.name}_change_handler` );
const handler = block.getUniqueName( `${state.parentNode}_change_handler` );
const isMultipleSelect = node.name === 'select' && node.attributes.find( attr => attr.name.toLowerCase() === 'multiple' ); // TODO use getStaticAttributeValue
const type = getStaticAttributeValue( node, 'type' );
const bindingGroup = attribute.name === 'group' ? getBindingGroup( generator, keypath ) : null;
const value = getBindingValue( generator, block, local, node, attribute, isMultipleSelect, bindingGroup, type );
const value = getBindingValue( generator, block, state, node, attribute, isMultipleSelect, bindingGroup, type );
const eventName = getBindingEventName( node );
let setter = getSetter({ block, name, keypath, context: '__svelte', attribute, dependencies, value });
let setter = getSetter({ block, name, keypath, context: '_svelte', attribute, dependencies, value });
let updateElement;
// <select> special case
if ( node.name === 'select' ) {
if ( !isMultipleSelect ) {
setter = `var selectedOption = ${local.name}.selectedOptions[0] || ${local.name}.options[0];\n${setter}`;
setter = `var selectedOption = ${state.parentNode}.selectedOptions[0] || ${state.parentNode}.options[0];\n${setter}`;
}
const value = block.getUniqueName( 'value' );
@ -45,8 +45,8 @@ export default function addElementBinding ( generator, node, attribute, block, l
updateElement = deindent`
var ${value} = ${snippet};
for ( var ${i} = 0; ${i} < ${local.name}.options.length; ${i} += 1 ) {
var ${option} = ${local.name}.options[${i}];
for ( var ${i} = 0; ${i} < ${state.parentNode}.options.length; ${i} += 1 ) {
var ${option} = ${state.parentNode}.options[${i}];
${ifStatement}
}
@ -57,34 +57,34 @@ export default function addElementBinding ( generator, node, attribute, block, l
else if ( attribute.name === 'group' ) {
if ( type === 'radio' ) {
setter = deindent`
if ( !${local.name}.checked ) return;
if ( !${state.parentNode}.checked ) return;
${setter}
`;
}
const condition = type === 'checkbox' ?
`~${snippet}.indexOf( ${local.name}.__value )` :
`${local.name}.__value === ${snippet}`;
`~${snippet}.indexOf( ${state.parentNode}.__value )` :
`${state.parentNode}.__value === ${snippet}`;
local.create.addLine(
`${block.component}._bindingGroups[${bindingGroup}].push( ${local.name} );`
block.builders.create.addLine(
`${block.component}._bindingGroups[${bindingGroup}].push( ${state.parentNode} );`
);
local.destroy.addBlock(
`${block.component}._bindingGroups[${bindingGroup}].splice( ${block.component}._bindingGroups[${bindingGroup}].indexOf( ${local.name} ), 1 );`
block.builders.destroy.addBlock(
`${block.component}._bindingGroups[${bindingGroup}].splice( ${block.component}._bindingGroups[${bindingGroup}].indexOf( ${state.parentNode} ), 1 );`
);
updateElement = `${local.name}.checked = ${condition};`;
updateElement = `${state.parentNode}.checked = ${condition};`;
}
// everything else
else {
updateElement = `${local.name}.${attribute.name} = ${snippet};`;
updateElement = `${state.parentNode}.${attribute.name} = ${snippet};`;
}
const updating = block.getUniqueName( `${local.name}_updating` );
const updating = block.getUniqueName( `${state.parentNode}_updating` );
local.create.addBlock( deindent`
block.builders.create.addBlock( deindent`
var ${updating} = false;
function ${handler} () {
@ -93,19 +93,19 @@ export default function addElementBinding ( generator, node, attribute, block, l
${updating} = false;
}
${generator.helper( 'addEventListener' )}( ${local.name}, '${eventName}', ${handler} );
${generator.helper( 'addEventListener' )}( ${state.parentNode}, '${eventName}', ${handler} );
` );
node.initialUpdate = updateElement;
local.update.addLine( deindent`
block.builders.update.addLine( deindent`
if ( !${updating} ) {
${updateElement}
}
` );
block.builders.destroy.addLine( deindent`
${generator.helper( 'removeEventListener' )}( ${local.name}, '${eventName}', ${handler} );
${generator.helper( 'removeEventListener' )}( ${state.parentNode}, '${eventName}', ${handler} );
` );
}
@ -124,10 +124,10 @@ function getBindingEventName ( node ) {
return 'change';
}
function getBindingValue ( generator, block, local, node, attribute, isMultipleSelect, bindingGroup, type ) {
function getBindingValue ( generator, block, state, node, attribute, isMultipleSelect, bindingGroup, type ) {
// <select multiple bind:value='selected>
if ( isMultipleSelect ) {
return `[].map.call( ${local.name}.selectedOptions, function ( option ) { return option.__value; })`;
return `[].map.call( ${state.parentNode}.selectedOptions, function ( option ) { return option.__value; })`;
}
// <select bind:value='selected>
@ -141,16 +141,16 @@ function getBindingValue ( generator, block, local, node, attribute, isMultipleS
return `${generator.helper( 'getBindingGroupValue' )}( ${block.component}._bindingGroups[${bindingGroup}] )`;
}
return `${local.name}.__value`;
return `${state.parentNode}.__value`;
}
// <input type='range|number' bind:value>
if ( type === 'range' || type === 'number' ) {
return `+${local.name}.${attribute.name}`;
return `+${state.parentNode}.${attribute.name}`;
}
// everything else
return `${local.name}.${attribute.name}`;
return `${state.parentNode}.${attribute.name}`;
}
function getBindingGroup ( generator, keypath ) {

@ -0,0 +1,121 @@
import deindent from '../../../../utils/deindent.js';
import visit from '../../visit.js';
import visitComponent from '../Component/Component.js';
import visitWindow from './meta/Window.js';
import visitAttribute from './Attribute.js';
import visitEventHandler from './EventHandler.js';
import visitBinding from './Binding.js';
import visitRef from './Ref.js';
const meta = {
':Window': visitWindow
};
const order = {
Attribute: 1,
EventHandler: 2,
Binding: 3,
Ref: 4
};
const visitors = {
Attribute: visitAttribute,
EventHandler: visitEventHandler,
Binding: visitBinding,
Ref: visitRef
};
export default function visitElement ( generator, block, state, node ) {
if ( node.name in meta ) {
return meta[ node.name ]( generator, block, node );
}
if ( generator.components.has( node.name ) || node.name === ':Self' ) {
return visitComponent( generator, block, state, node );
}
const name = block.getUniqueName( node.name );
const childState = Object.assign( {}, state, {
isTopLevel: false,
parentNode: name,
namespace: node.name === 'svg' ? 'http://www.w3.org/2000/svg' : state.namespace,
allUsedContexts: []
});
block.builders.create.addLine( `var ${name} = ${getRenderStatement( generator, childState.namespace, node.name )};` );
block.mount( name, state.parentNode );
if ( !state.parentNode ) {
block.builders.detach.addLine( `${generator.helper( 'detachNode' )}( ${name} );` );
}
// add CSS encapsulation attribute
if ( generator.cssId && state.isTopLevel ) {
block.builders.create.addLine( `${generator.helper( 'setAttribute' )}( ${name}, '${generator.cssId}', '' );` );
}
node.attributes
.sort( ( a, b ) => order[ a.type ] - order[ b.type ] )
.forEach( attribute => {
visitors[ attribute.type ]( generator, block, childState, node, attribute );
});
// special case bound <option> without a value attribute
if ( node.name === 'option' && !node.attributes.find( attribute => attribute.type === 'Attribute' && attribute.name === 'value' ) ) { // TODO check it's bound
const statement = `${name}.__value = ${name}.textContent;`;
block.builders.update.addLine( statement );
node.initialUpdate = statement;
}
if ( node.initialUpdate ) {
block.builders.create.addBlock( node.initialUpdate );
}
if ( childState.allUsedContexts.length || childState.usesComponent ) {
const initialProps = [];
const updates = [];
if ( childState.usesComponent ) {
initialProps.push( `component: ${block.component}` );
}
childState.allUsedContexts.forEach( contextName => {
if ( contextName === 'root' ) return;
const listName = block.listNames.get( contextName );
const indexName = block.indexNames.get( contextName );
initialProps.push( `${listName}: ${listName},\n${indexName}: ${indexName}` );
updates.push( `${name}._svelte.${listName} = ${listName};\n${name}._svelte.${indexName} = ${indexName};` );
});
if ( initialProps.length ) {
block.builders.create.addBlock( deindent`
${name}._svelte = {
${initialProps.join( ',\n' )}
};
` );
}
if ( updates.length ) {
block.builders.update.addBlock( updates.join( '\n' ) );
}
}
node.children.forEach( child => {
visit( generator, block, childState, child );
});
}
function getRenderStatement ( generator, namespace, name ) {
if ( namespace === 'http://www.w3.org/2000/svg' ) {
return `${generator.helper( 'createSvgElement' )}( '${name}' )`;
}
if ( namespace ) {
return `document.createElementNS( '${namespace}', '${name}' )`;
}
return `${generator.helper( 'createElement' )}( '${name}' )`;
}

@ -0,0 +1,97 @@
import deindent from '../../../../utils/deindent.js';
import CodeBuilder from '../../../../utils/CodeBuilder.js';
import flattenReference from '../../../../utils/flattenReference.js';
export default function visitEventHandler ( generator, block, state, node, attribute ) {
const name = attribute.name;
const isCustomEvent = generator.events.has( name );
const shouldHoist = !isCustomEvent && state.inEachBlock;
generator.addSourcemapLocations( attribute.expression );
const flattened = flattenReference( attribute.expression.callee );
if ( flattened.name !== 'event' && flattened.name !== 'this' ) {
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.code.prependRight( attribute.expression.start, `${block.component}.` );
if ( shouldHoist ) state.usesComponent = true; // this feels a bit hacky but it works!
}
const context = shouldHoist ? null : state.parentNode;
const usedContexts = [];
attribute.expression.arguments.forEach( arg => {
const { contexts } = block.contextualise( arg, context, true );
contexts.forEach( context => {
if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context );
if ( !~state.allUsedContexts.indexOf( context ) ) state.allUsedContexts.push( context );
});
});
const _this = context || 'this';
const declarations = usedContexts.map( name => {
if ( name === 'root' ) {
if ( shouldHoist ) state.usesComponent = true;
return 'var root = component.get();';
}
const listName = block.listNames.get( name );
const indexName = block.indexNames.get( name );
return `var ${listName} = ${_this}._svelte.${listName}, ${indexName} = ${_this}._svelte.${indexName}, ${name} = ${listName}[${indexName}];`;
});
// get a name for the event handler that is globally unique
// if hoisted, locally unique otherwise
const handlerName = shouldHoist ?
generator.alias( `${name}_handler` ) :
block.getUniqueName( `${name}_handler` );
// create the handler body
const handlerBody = new CodeBuilder();
if ( state.usesComponent ) {
// TODO the element needs to know to create `thing._svelte = { component: component }`
handlerBody.addLine( `var component = this._svelte.component;` );
}
declarations.forEach( declaration => {
handlerBody.addLine( declaration );
});
handlerBody.addLine( `[✂${attribute.expression.start}-${attribute.expression.end}✂];` );
const handler = isCustomEvent ?
deindent`
var ${handlerName} = ${generator.alias( 'template' )}.events.${name}.call( ${block.component}, ${state.parentNode}, function ( event ) {
${handlerBody}
});
` :
deindent`
function ${handlerName} ( event ) {
${handlerBody}
}
`;
if ( shouldHoist ) {
generator.addBlock({
render: () => handler
});
} else {
block.builders.create.addBlock( handler );
}
if ( isCustomEvent ) {
block.builders.destroy.addLine( deindent`
${handlerName}.teardown();
` );
} else {
block.builders.create.addLine( deindent`
${generator.helper( 'addEventListener' )}( ${state.parentNode}, '${name}', ${handlerName} );
` );
block.builders.destroy.addLine( deindent`
${generator.helper( 'removeEventListener' )}( ${state.parentNode}, '${name}', ${handlerName} );
` );
}
}

@ -0,0 +1,15 @@
import deindent from '../../../../utils/deindent.js';
export default function visitRef ( generator, block, state, node, attribute ) {
const name = attribute.name;
block.builders.create.addLine(
`${block.component}.refs.${name} = ${state.parentNode};`
);
block.builders.destroy.addLine( deindent`
if ( ${block.component}.refs.${name} === ${state.parentNode} ) ${block.component}.refs.${name} = null;
` );
generator.usesRefs = true; // so this component.refs object is created
}

@ -1,5 +1,5 @@
import flattenReference from '../../../../utils/flattenReference.js';
import deindent from '../../../../utils/deindent.js';
import flattenReference from '../../../../../utils/flattenReference.js';
import deindent from '../../../../../utils/deindent.js';
const associatedEvents = {
innerWidth: 'resize',

@ -1,132 +0,0 @@
import addComponentBinding from './addComponentBinding.js';
import deindent from '../../../../utils/deindent.js';
export default function addComponentAttributes ( generator, block, node, local ) {
local.staticAttributes = [];
local.dynamicAttributes = [];
local.bindings = [];
node.attributes.forEach( attribute => {
if ( attribute.type === 'Attribute' ) {
if ( attribute.value === true ) {
// attributes without values, e.g. <textarea readonly>
local.staticAttributes.push({
name: attribute.name,
value: true
});
}
else if ( attribute.value.length === 0 ) {
local.staticAttributes.push({
name: attribute.name,
value: `''`
});
}
else if ( attribute.value.length === 1 ) {
const value = attribute.value[0];
if ( value.type === 'Text' ) {
// static attributes
const result = isNaN( value.data ) ? JSON.stringify( value.data ) : value.data;
local.staticAttributes.push({
name: attribute.name,
value: result
});
}
else {
// simple dynamic attributes
const { dependencies, string } = generator.contextualise( block, value.expression );
// TODO only update attributes that have changed
local.dynamicAttributes.push({
name: attribute.name,
value: string,
dependencies
});
}
}
else {
// complex dynamic attributes
const allDependencies = [];
const value = ( attribute.value[0].type === 'Text' ? '' : `"" + ` ) + (
attribute.value.map( chunk => {
if ( chunk.type === 'Text' ) {
return JSON.stringify( chunk.data );
} else {
const { dependencies, string } = generator.contextualise( block, chunk.expression );
dependencies.forEach( dependency => {
if ( !~allDependencies.indexOf( dependency ) ) allDependencies.push( dependency );
});
return `( ${string} )`;
}
}).join( ' + ' )
);
local.dynamicAttributes.push({
name: attribute.name,
value,
dependencies: allDependencies
});
}
}
else if ( attribute.type === 'EventHandler' ) {
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.addSourcemapLocations( attribute.expression );
generator.code.prependRight( attribute.expression.start, `${block.component}.` );
const usedContexts = [];
attribute.expression.arguments.forEach( arg => {
const { contexts } = generator.contextualise( block, arg, true );
contexts.forEach( context => {
if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context );
if ( !~local.allUsedContexts.indexOf( context ) ) local.allUsedContexts.push( context );
});
});
// TODO hoist event handlers? can do `this.__component.method(...)`
const declarations = usedContexts.map( name => {
if ( name === 'root' ) return 'var root = this._context.root;';
const listName = block.listNames.get( name );
const indexName = block.indexNames.get( name );
return `var ${listName} = this._context.${listName}, ${indexName} = this._context.${indexName}, ${name} = ${listName}[${indexName}]`;
});
const handlerBody = ( declarations.length ? declarations.join( '\n' ) + '\n\n' : '' ) + `[✂${attribute.expression.start}-${attribute.expression.end}✂];`;
local.create.addBlock( deindent`
${local.name}.on( '${attribute.name}', function ( event ) {
${handlerBody}
});
` );
}
else if ( attribute.type === 'Binding' ) {
addComponentBinding( generator, node, attribute, block, local );
}
else if ( attribute.type === 'Ref' ) {
generator.usesRefs = true;
local.create.addLine(
`${block.component}.refs.${attribute.name} = ${local.name};`
);
block.builders.destroy.addLine( deindent`
if ( ${block.component}.refs.${attribute.name} === ${local.name} ) ${block.component}.refs.${attribute.name} = null;
` );
}
else {
throw new Error( `Not implemented: ${attribute.type}` );
}
});
}

@ -1,229 +0,0 @@
import attributeLookup from './lookup.js';
import addElementBinding from './addElementBinding';
import deindent from '../../../../utils/deindent.js';
import flattenReference from '../../../../utils/flattenReference.js';
import getStaticAttributeValue from './binding/getStaticAttributeValue.js';
export default function addElementAttributes ( generator, block, node, local ) {
node.attributes.forEach( attribute => {
const name = attribute.name;
if ( attribute.type === 'Attribute' ) {
let metadata = local.namespace ? null : attributeLookup[ name ];
if ( metadata && metadata.appliesTo && !~metadata.appliesTo.indexOf( node.name ) ) metadata = null;
let dynamic = false;
const isIndirectlyBoundValue = name === 'value' && (
node.name === 'option' || // TODO check it's actually bound
node.name === 'input' && /^(checkbox|radio)$/.test( getStaticAttributeValue( node, 'type' ) )
);
const propertyName = isIndirectlyBoundValue ? '__value' : metadata && metadata.propertyName;
const isXlink = name.slice( 0, 6 ) === 'xlink:';
// xlink is a special case... we could maybe extend this to generic
// namespaced attributes but I'm not sure that's applicable in
// HTML5?
const method = isXlink ? 'setXlinkAttribute' : 'setAttribute';
if ( attribute.value === true ) {
// attributes without values, e.g. <textarea readonly>
if ( propertyName ) {
local.create.addLine(
`${local.name}.${propertyName} = true;`
);
} else {
local.create.addLine(
`${generator.helper( method )}( ${local.name}, '${name}', true );`
);
}
// special case autofocus. has to be handled in a bit of a weird way
if ( name === 'autofocus' ) {
block.autofocus = local.name;
}
}
else if ( attribute.value.length === 0 ) {
if ( propertyName ) {
local.create.addLine(
`${local.name}.${propertyName} = '';`
);
} else {
local.create.addLine(
`${generator.helper( method )}( ${local.name}, '${name}', '' );`
);
}
}
else if ( attribute.value.length === 1 ) {
const value = attribute.value[0];
let result = '';
if ( value.type === 'Text' ) {
// static attributes
result = JSON.stringify( value.data );
let addAttribute = false;
if ( name === 'xmlns' ) {
// special case
// TODO this attribute must be static enforce at compile time
local.namespace = value.data;
addAttribute = true;
} else if ( propertyName ) {
local.create.addLine(
`${local.name}.${propertyName} = ${result};`
);
} else {
addAttribute = true;
}
if ( addAttribute ) {
local.create.addLine(
`${generator.helper( method )}( ${local.name}, '${name}', ${result} );`
);
}
}
else {
dynamic = true;
// dynamic but potentially non-string attributes
const { snippet } = generator.contextualise( block, value.expression );
const last = `last_${local.name}_${name.replace( /-/g, '_')}`;
local.create.addLine( `var ${last} = ${snippet};` );
let updater;
if ( propertyName ) {
updater = `${local.name}.${propertyName} = ${last};`;
} else {
updater = `${generator.helper( method )}( ${local.name}, '${name}', ${last} );`;
}
local.create.addLine( updater );
local.update.addBlock( deindent`
if ( ( ${block.tmp()} = ${snippet} ) !== ${last} ) {
${last} = ${block.tmp()};
${updater}
}
` );
}
}
else {
dynamic = true;
const value = ( attribute.value[0].type === 'Text' ? '' : `"" + ` ) + (
attribute.value.map( chunk => {
if ( chunk.type === 'Text' ) {
return JSON.stringify( chunk.data );
} else {
const { snippet } = generator.contextualise( block, chunk.expression );
return `( ${snippet} )`;
}
}).join( ' + ' )
);
let updater;
if (propertyName) {
updater = `${local.name}.${propertyName} = ${value};`;
} else {
updater = `${generator.helper( method )}( ${local.name}, '${name}', ${value} );`;
}
local.create.addLine( updater );
local.update.addLine( updater );
}
if ( isIndirectlyBoundValue ) {
const updateValue = `${local.name}.value = ${local.name}.__value;`;
local.create.addLine( updateValue );
if ( dynamic ) local.update.addLine( updateValue );
}
}
else if ( attribute.type === 'EventHandler' ) {
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.addSourcemapLocations( attribute.expression );
const flattened = flattenReference( attribute.expression.callee );
if ( flattened.name !== 'event' && flattened.name !== 'this' ) {
// allow event.stopPropagation(), this.select() etc
generator.code.prependRight( attribute.expression.start, `${block.component}.` );
}
const usedContexts = [];
attribute.expression.arguments.forEach( arg => {
const { contexts } = generator.contextualise( block, arg, true );
contexts.forEach( context => {
if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context );
if ( !~local.allUsedContexts.indexOf( context ) ) local.allUsedContexts.push( context );
});
});
// TODO hoist event handlers? can do `this.__component.method(...)`
const declarations = usedContexts.map( name => {
if ( name === 'root' ) return 'var root = this.__svelte.root;';
const listName = block.listNames.get( name );
const indexName = block.indexNames.get( name );
return `var ${listName} = this.__svelte.${listName}, ${indexName} = this.__svelte.${indexName}, ${name} = ${listName}[${indexName}]`;
});
const handlerName = block.getUniqueName( `${name}_handler` );
const handlerBody = ( declarations.length ? declarations.join( '\n' ) + '\n\n' : '' ) + `[✂${attribute.expression.start}-${attribute.expression.end}✂];`;
if ( generator.events.has( name ) ) {
local.create.addBlock( deindent`
var ${handlerName} = ${generator.alias( 'template' )}.events.${name}.call( ${block.component}, ${local.name}, function ( event ) {
${handlerBody}
}.bind( ${local.name} ) );
` );
block.builders.destroy.addLine( deindent`
${handlerName}.teardown();
` );
} else {
local.create.addBlock( deindent`
function ${handlerName} ( event ) {
${handlerBody}
}
${generator.helper( 'addEventListener' )}( ${local.name}, '${name}', ${handlerName} );
` );
block.builders.destroy.addLine( deindent`
${generator.helper( 'removeEventListener' )}( ${local.name}, '${name}', ${handlerName} );
` );
}
}
else if ( attribute.type === 'Binding' ) {
addElementBinding( generator, node, attribute, block, local );
}
else if ( attribute.type === 'Ref' ) {
generator.usesRefs = true;
local.create.addLine(
`${block.component}.refs.${name} = ${local.name};`
);
block.builders.destroy.addLine( deindent`
if ( ${block.component}.refs.${name} === ${local.name} ) ${block.component}.refs.${name} = null;
` );
}
else {
throw new Error( `Not implemented: ${attribute.type}` );
}
});
}

@ -1,6 +1,6 @@
import Comment from './Comment.js';
import EachBlock from './EachBlock.js';
import Element from './Element.js';
import Element from './Element/Element.js';
import IfBlock from './IfBlock.js';
import MustacheTag from './MustacheTag.js';
import RawMustacheTag from './RawMustacheTag.js';

@ -17,7 +17,7 @@ export function detachBetween ( before, after ) {
}
export function destroyEach ( iterations, detach, start ) {
for ( var i = ( start || 0 ); i < iterations.length; i += 1 ) {
for ( var i = start; i < iterations.length; i += 1 ) {
iterations[i].destroy( detach );
}
}

@ -18,11 +18,17 @@ describe( 'js', () => {
dir = path.resolve( 'test/js/samples', dir );
const input = fs.readFileSync( `${dir}/input.html`, 'utf-8' ).replace( /\s+$/, '' );
const actual = svelte.compile( input ).code;
const actual = svelte.compile( input, {
shared: true
}).code;
fs.writeFileSync( `${dir}/_actual.js`, actual );
const expected = fs.readFileSync( `${dir}/expected.js`, 'utf-8' );
assert.equal( actual.trim(), expected.trim() );
assert.equal(
actual.trim().replace( /^\s+$/gm, '' ),
expected.trim().replace( /^\s+$/gm, '' )
);
});
});
});

@ -0,0 +1,86 @@
import { appendNode, assign, createElement, createText, detachNode, dispatchObservers, insertNode, noop, proto } from "svelte/shared.js";
var template = (function () {
return {
methods: {
foo ( bar ) {
console.log( bar );
}
},
events: {
foo ( node, callback ) {
// code goes here
}
}
};
}());
function create_main_fragment ( root, component ) {
var button = createElement( 'button' );
var foo_handler = template.events.foo.call( component, button, function ( event ) {
var root = component.get();
component.foo( root.bar );
});
appendNode( createText( "foo" ), button );
return {
mount: function ( target, anchor ) {
insertNode( button, target, anchor );
},
update: noop,
destroy: function ( detach ) {
foo_handler.teardown();
if ( detach ) {
detachNode( button );
}
}
};
}
function SvelteComponent ( options ) {
options = options || {};
this._state = options.data || {};
this._observers = {
pre: Object.create( null ),
post: Object.create( null )
};
this._handlers = Object.create( null );
this._root = options._root;
this._yield = options._yield;
this._torndown = false;
this._fragment = create_main_fragment( this._state, this );
if ( options.target ) this._fragment.mount( options.target, null );
}
assign( SvelteComponent.prototype, template.methods, proto );
SvelteComponent.prototype._set = function _set ( newState ) {
var oldState = this._state;
this._state = assign( {}, oldState, newState );
dispatchObservers( this, this._observers.pre, newState, oldState );
if ( this._fragment ) this._fragment.update( newState, this._state );
dispatchObservers( this, this._observers.post, newState, oldState );
};
SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
this.fire( 'destroy' );
this._fragment.destroy( detach !== false );
this._fragment = null;
this._state = {};
this._torndown = true;
};
export default SvelteComponent;

@ -0,0 +1,16 @@
<button on:foo='foo( bar )'>foo</button>
<script>
export default {
methods: {
foo ( bar ) {
console.log( bar );
}
},
events: {
foo ( node, callback ) {
// code goes here
}
}
};
</script>

@ -0,0 +1,32 @@
export default {
html: `
<button>foo</button>
<button>bar</button>
<button>baz</button>
<p>fromDom: </p>
<p>fromState: </p>
`,
test ( assert, component, target, window ) {
const event = new window.MouseEvent( 'click' );
const buttons = target.querySelectorAll( 'button' );
buttons[1].dispatchEvent( event );
assert.htmlEqual( target.innerHTML, `
<button>foo</button>
<button>bar</button>
<button>baz</button>
<p>fromDom: bar</p>
<p>fromState: bar</p>
` );
assert.equal( component.get( 'fromDom' ), 'bar' );
assert.equal( component.get( 'fromState' ), 'bar' );
component.destroy();
}
};

@ -0,0 +1,34 @@
{{#each items as item}}
<button on:tap='set({ fromDom: this.textContent, fromState: item })'>{{item}}</button>
{{/each}}
<p>fromDom: {{fromDom}}</p>
<p>fromState: {{fromState}}</p>
<script>
export default {
data: () => ({
x: 0,
y: 0,
fromDom: '',
fromState: '',
items: [ 'foo', 'bar', 'baz' ]
}),
events: {
tap ( node, callback ) {
function clickHandler ( event ) {
callback();
}
node.addEventListener( 'click', clickHandler, false );
return {
teardown () {
node.addEventListener( 'click', clickHandler, false );
}
};
}
}
};
</script>

@ -0,0 +1,32 @@
export default {
data: {
items: [
'foo',
'bar',
'baz'
],
selected: 'foo'
},
html: `
<button>foo</button>
<button>bar</button>
<button>baz</button>
<p>selected: foo</p>
`,
test ( assert, component, target, window ) {
const buttons = target.querySelectorAll( 'button' );
const event = new window.MouseEvent( 'click' );
buttons[1].dispatchEvent( event );
assert.htmlEqual( target.innerHTML, `
<button>foo</button>
<button>bar</button>
<button>baz</button>
<p>selected: bar</p>
` );
component.destroy();
}
};

@ -0,0 +1,5 @@
{{#each items as item}}
<button on:click='set({ selected: item })'>{{item}}</button>
{{/each}}
<p>selected: {{selected}}</p>

@ -1,13 +1,21 @@
export default {
html: '<button>toggle</button>\n\n<!---->',
html: `
<button>toggle</button>
`,
test ( assert, component, target, window ) {
const button = target.querySelector( 'button' );
const event = new window.MouseEvent( 'click' );
button.dispatchEvent( event );
assert.equal( target.innerHTML, '<button>toggle</button>\n\n<p>hello!</p><!---->' );
assert.htmlEqual( target.innerHTML, `
<button>toggle</button>
<p>hello!</p>
` );
button.dispatchEvent( event );
assert.equal( target.innerHTML, '<button>toggle</button>\n\n<!---->' );
assert.htmlEqual( target.innerHTML, `
<button>toggle</button>
` );
}
};

Loading…
Cancel
Save