mirror of https://github.com/sveltejs/svelte
Merge pull request #456 from sveltejs/gh-433-b
Avoid binding event handler callbacks, version 2pull/457/head
commit
20298b1a0a
@ -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
|
||||
});
|
||||
}
|
||||
}
|
@ -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;
|
||||
` );
|
||||
}
|
@ -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}` );
|
||||
}
|
||||
});
|
||||
}
|
@ -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…
Reference in new issue