custom methods

pull/31/head
Rich-Harris 8 years ago
parent 6dbc777797
commit 83f0f7d202

@ -3,6 +3,7 @@ import { walk } from 'estree-walker';
import deindent from './utils/deindent.js'; import deindent from './utils/deindent.js';
import walkHtml from './utils/walkHtml.js'; import walkHtml from './utils/walkHtml.js';
import flattenReference from './utils/flattenReference.js'; import flattenReference from './utils/flattenReference.js';
import contextualise from './utils/contextualise.js';
import counter from './utils/counter.js'; import counter from './utils/counter.js';
function createRenderer ( fragment ) { function createRenderer ( fragment ) {
@ -26,22 +27,28 @@ function createRenderer ( fragment ) {
export default function generate ( parsed, template ) { export default function generate ( parsed, template ) {
const code = new MagicString( template ); const code = new MagicString( template );
let hasDefaultData = false; function addSourcemapLocations ( node ) {
walk( node, {
// TODO wrap all this in magic-string
if ( parsed.js ) {
walk( parsed.js.content, {
enter ( node ) { enter ( node ) {
code.addSourcemapLocation( node.start ); code.addSourcemapLocation( node.start );
code.addSourcemapLocation( node.end ); code.addSourcemapLocation( node.end );
} }
}); });
}
const templateProperties = {};
if ( parsed.js ) {
addSourcemapLocations( parsed.js.content );
const defaultExport = parsed.js.content.body.find( node => node.type === 'ExportDefaultDeclaration' ); const defaultExport = parsed.js.content.body.find( node => node.type === 'ExportDefaultDeclaration' );
if ( defaultExport ) { if ( defaultExport ) {
code.overwrite( defaultExport.start, defaultExport.declaration.start, `const template = ` ); code.overwrite( defaultExport.start, defaultExport.declaration.start, `const template = ` );
hasDefaultData = defaultExport.declaration.properties.find( prop => prop.key.name === 'data' );
defaultExport.declaration.properties.forEach( prop => {
templateProperties[ prop.key.name ] = true;
});
} }
} }
@ -65,7 +72,7 @@ export default function generate ( parsed, template ) {
teardownStatements: [], teardownStatements: [],
contexts: {}, contexts: {},
contextChain: [ 'context' ], contextChain: [ 'root' ],
counter: counter(), counter: counter(),
@ -78,7 +85,7 @@ export default function generate ( parsed, template ) {
if ( flattened ) { if ( flattened ) {
if ( flattened.name in contexts ) return flattened.keypath; if ( flattened.name in contexts ) return flattened.keypath;
// TODO handle globals, e.g. {{Math.round(foo)}} // TODO handle globals, e.g. {{Math.round(foo)}}
return `context.${flattened.keypath}`; return `root.${flattened.keypath}`;
} }
console.log( `node, contexts`, node, contexts ) console.log( `node, contexts`, node, contexts )
@ -96,31 +103,53 @@ export default function generate ( parsed, template ) {
`var ${name} = document.createElement( '${node.name}' );` `var ${name} = document.createElement( '${node.name}' );`
]; ];
const updateStatements = [];
const teardownStatements = [ const teardownStatements = [
`${name}.parentNode.removeChild( ${name} );` `${name}.parentNode.removeChild( ${name} );`
]; ];
const allUsedContexts = new Set();
node.attributes.forEach( attribute => { node.attributes.forEach( attribute => {
if ( attribute.type === 'EventHandler' ) { if ( attribute.type === 'EventHandler' ) {
// TODO use magic-string here, so that stack traces
// go through the template
// TODO verify that it's a valid callee (i.e. built-in or declared method) // TODO verify that it's a valid callee (i.e. built-in or declared method)
const handler = current.counter( `${attribute.name}Handler` ); const handler = current.counter( `${attribute.name}Handler` );
const callee = `component.${attribute.expression.callee.name}`; addSourcemapLocations( attribute.expression );
const args = attribute.expression.arguments code.insertRight( attribute.expression.start, 'component.' );
.map( arg => flattenExpression( arg, current.contexts ) )
.join( ', ' ); const usedContexts = new Set();
attribute.expression.arguments.forEach( arg => {
initStatements.push( deindent` const contexts = contextualise( code, arg, current.contexts );
function ${handler} ( event ) {
${callee}(${args}); contexts.forEach( context => {
} usedContexts.add( context );
allUsedContexts.add( context );
${name}.addEventListener( '${attribute.name}', ${handler}, false ); });
` ); });
// TODO hoist event handlers? can do `this.__component.method(...)`
if ( usedContexts.size ) {
initStatements.push( deindent`
function ${handler} ( event ) {
var context = this.__context;
${[...usedContexts].map( name => `var ${name} = context.${name}` ).join( '\n' )}
[${attribute.expression.start}-${attribute.expression.end}];
}
${name}.addEventListener( '${attribute.name}', ${handler}, false );
` );
} else {
initStatements.push( deindent`
function ${handler} ( event ) {
[${attribute.expression.start}-${attribute.expression.end}];
}
${name}.addEventListener( '${attribute.name}', ${handler}, false );
` );
}
teardownStatements.push( deindent` teardownStatements.push( deindent`
${name}.removeEventListener( '${attribute.name}', ${handler}, false ); ${name}.removeEventListener( '${attribute.name}', ${handler}, false );
@ -132,7 +161,18 @@ export default function generate ( parsed, template ) {
} }
}); });
if ( allUsedContexts.size ) {
initStatements.push( deindent`
${name}.__context = {};
` );
updateStatements.push( deindent`
${[...allUsedContexts].map( contextName => `${name}.__context.${contextName} = ${contextName};` ).join( '\n' )}
` );
}
current.initStatements.push( initStatements.join( '\n' ) ); current.initStatements.push( initStatements.join( '\n' ) );
if ( updateStatements.length ) current.updateStatements.push( updateStatements.join( '\n' ) );
current.teardownStatements.push( teardownStatements.join( '\n' ) ); current.teardownStatements.push( teardownStatements.join( '\n' ) );
current = Object.assign( {}, current, { current = Object.assign( {}, current, {
@ -211,7 +251,7 @@ export default function generate ( parsed, template ) {
} }
if ( ${name} ) { if ( ${name} ) {
${name}.update( context ); ${name}.update( ${current.contextChain.join( '\n' )} );
} }
` ); ` );
@ -319,7 +359,7 @@ export default function generate ( parsed, template ) {
${renderers.reverse().join( '\n\n' )} ${renderers.reverse().join( '\n\n' )}
export default function createComponent ( options ) { export default function createComponent ( options ) {
var component = {}; var component = ${templateProperties.methods ? `Object.create( template.methods )` : `{}`};
var state = {}; var state = {};
var observers = { var observers = {
@ -374,13 +414,13 @@ export default function generate ( parsed, template ) {
}; };
let mainFragment = renderMainFragment( component, options.target ); let mainFragment = renderMainFragment( component, options.target );
component.set( ${hasDefaultData ? `Object.assign( template.data(), options.data )` : `options.data`} ); component.set( ${templateProperties.data ? `Object.assign( template.data(), options.data )` : `options.data`} );
return component; return component;
} }
`; `;
const pattern = /\[✂(\d+)-(\d+)/; const pattern = /\[✂(\d+)-(\d+)$/;
const parts = result.split( '✂]' ); const parts = result.split( '✂]' );
const finalChunk = parts.pop(); const finalChunk = parts.pop();
@ -404,7 +444,7 @@ export default function generate ( parsed, template ) {
sortedBySource.forEach( part => { sortedBySource.forEach( part => {
code.remove( c, part.start ); code.remove( c, part.start );
code.insertLeft( part.start, part.chunk ); code.insertRight( part.start, part.chunk );
c = part.end; c = part.end;
}); });

@ -0,0 +1,23 @@
import { walk } from 'estree-walker';
import isReference from './isReference.js';
import flattenReference from './flattenReference.js';
export default function contextualise ( code, expression, contexts ) {
const usedContexts = [];
walk( expression, {
enter ( node, parent ) {
if ( isReference( node, parent ) ) {
const { name } = flattenReference( node );
if ( contexts[ name ] ) {
if ( !~usedContexts.indexOf( name ) ) usedContexts.push( name );
} else {
code.insertRight( node.start, `root.` );
if ( !~usedContexts.indexOf( 'root' ) ) usedContexts.push( 'root' );
}
}
}
});
return usedContexts;
}

@ -0,0 +1,19 @@
import * as assert from 'assert';
export default {
html: '<button>+1</button><p>0</p>',
test ( component, target, window ) {
const button = target.querySelector( 'button' );
const event = new window.MouseEvent( 'click' );
button.dispatchEvent( event );
assert.equal( component.get( 'counter' ), 1 );
assert.equal( target.innerHTML, '<button>+1</button><p>1</p>' );
button.dispatchEvent( event );
assert.equal( component.get( 'counter' ), 2 );
assert.equal( target.innerHTML, '<button>+1</button><p>2</p>' );
assert.equal( component.foo(), 42 );
}
};

@ -0,0 +1,21 @@
<button on:click='add1()'>+1</button>
<p>{{counter}}</p>
<script>
export default {
data: () => ({
counter: 0
}),
methods: {
add1 () {
this.set({ counter: this.get( 'counter' ) + 1 });
},
foo () {
return 42;
}
}
};
</script>

@ -1,6 +1,5 @@
import * as assert from 'assert'; import * as assert from 'assert';
export default { export default {
description: 'hello world',
html: '<h1>Hello world!</h1>' html: '<h1>Hello world!</h1>'
}; };

@ -1,7 +1,6 @@
import * as assert from 'assert'; import * as assert from 'assert';
export default { export default {
description: '{{#each}}...{{/each}} block',
data: { data: {
animals: [ 'alpaca', 'baboon', 'capybara' ] animals: [ 'alpaca', 'baboon', 'capybara' ]
}, },

@ -1,5 +1,4 @@
export default { export default {
description: 'nested {{#each}} blocks where inner iterates over property of outer',
data: { data: {
categories: [ categories: [
{ {

@ -1,5 +1,4 @@
export default { export default {
description: 'nested {{#each}} blocks',
data: { data: {
columns: [ 'a', 'b', 'c' ], columns: [ 'a', 'b', 'c' ],
rows: [ 1, 2, 3 ] rows: [ 1, 2, 3 ]

@ -1,9 +1,15 @@
import * as assert from 'assert'; import * as assert from 'assert';
export default { export default {
description: 'attaches event handlers',
html: '<button>toggle</button><!--#if visible-->', html: '<button>toggle</button><!--#if visible-->',
test ( component, target ) { test ( component, target, window ) {
assert.ok( false, 'TODO fire synthetic event to test' ); const button = target.querySelector( 'button' );
const event = new window.MouseEvent( 'click' );
button.dispatchEvent( event );
assert.equal( target.innerHTML, '<button>toggle</button><p>hello!</p><!--#if visible-->' );
button.dispatchEvent( event );
assert.equal( target.innerHTML, '<button>toggle</button><!--#if visible-->' );
} }
}; };

@ -1,7 +1,6 @@
import * as assert from 'assert'; import * as assert from 'assert';
export default { export default {
description: 'hello world',
data: { data: {
name: 'world' name: 'world'
}, },

@ -1,7 +1,6 @@
import * as assert from 'assert'; import * as assert from 'assert';
export default { export default {
description: '{{#if}}...{{/if}} block',
data: { data: {
visible: true visible: true
}, },

@ -1,4 +1,3 @@
export default { export default {
description: 'creates a factory with a single static element',
html: '<span>test</span>' html: '<span>test</span>'
}; };

@ -123,7 +123,7 @@ describe( 'svelte', () => {
} }
if ( config.test ) { if ( config.test ) {
config.test( component, target ); config.test( component, target, window );
} else { } else {
component.teardown(); component.teardown();
assert.equal( target.innerHTML, '' ); assert.equal( target.innerHTML, '' );

Loading…
Cancel
Save