From 83f0f7d202a1a6043d761041372938571ec222b1 Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Sat, 19 Nov 2016 09:17:09 -0500 Subject: [PATCH] custom methods --- compiler/generate/index.js | 98 +++++++++++++------ compiler/generate/utils/contextualise.js | 23 +++++ test/compiler/custom-method/_config.js | 19 ++++ test/compiler/custom-method/main.svelte | 21 ++++ test/compiler/default-data/_config.js | 1 - test/compiler/each-block/_config.js | 1 - test/compiler/each-blocks-nested-b/_config.js | 1 - test/compiler/each-blocks-nested/_config.js | 1 - test/compiler/event-handler/_config.js | 12 ++- test/compiler/hello-world/_config.js | 1 - test/compiler/if-block/_config.js | 1 - .../compiler/single-static-element/_config.js | 1 - test/test.js | 2 +- 13 files changed, 142 insertions(+), 40 deletions(-) create mode 100644 compiler/generate/utils/contextualise.js create mode 100644 test/compiler/custom-method/_config.js create mode 100644 test/compiler/custom-method/main.svelte diff --git a/compiler/generate/index.js b/compiler/generate/index.js index eafb3b2038..0acf6d47da 100644 --- a/compiler/generate/index.js +++ b/compiler/generate/index.js @@ -3,6 +3,7 @@ import { walk } from 'estree-walker'; import deindent from './utils/deindent.js'; import walkHtml from './utils/walkHtml.js'; import flattenReference from './utils/flattenReference.js'; +import contextualise from './utils/contextualise.js'; import counter from './utils/counter.js'; function createRenderer ( fragment ) { @@ -26,22 +27,28 @@ function createRenderer ( fragment ) { export default function generate ( parsed, template ) { const code = new MagicString( template ); - let hasDefaultData = false; - - // TODO wrap all this in magic-string - if ( parsed.js ) { - walk( parsed.js.content, { + function addSourcemapLocations ( node ) { + walk( node, { enter ( node ) { code.addSourcemapLocation( node.start ); code.addSourcemapLocation( node.end ); } }); + } + + const templateProperties = {}; + + if ( parsed.js ) { + addSourcemapLocations( parsed.js.content ); const defaultExport = parsed.js.content.body.find( node => node.type === 'ExportDefaultDeclaration' ); if ( defaultExport ) { 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: [], contexts: {}, - contextChain: [ 'context' ], + contextChain: [ 'root' ], counter: counter(), @@ -78,7 +85,7 @@ export default function generate ( parsed, template ) { if ( flattened ) { if ( flattened.name in contexts ) return flattened.keypath; // TODO handle globals, e.g. {{Math.round(foo)}} - return `context.${flattened.keypath}`; + return `root.${flattened.keypath}`; } console.log( `node, contexts`, node, contexts ) @@ -96,31 +103,53 @@ export default function generate ( parsed, template ) { `var ${name} = document.createElement( '${node.name}' );` ]; + const updateStatements = []; + const teardownStatements = [ `${name}.parentNode.removeChild( ${name} );` ]; + const allUsedContexts = new Set(); + node.attributes.forEach( attribute => { 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) - const handler = current.counter( `${attribute.name}Handler` ); - const callee = `component.${attribute.expression.callee.name}`; - const args = attribute.expression.arguments - .map( arg => flattenExpression( arg, current.contexts ) ) - .join( ', ' ); - - initStatements.push( deindent` - function ${handler} ( event ) { - ${callee}(${args}); - } - - ${name}.addEventListener( '${attribute.name}', ${handler}, false ); - ` ); + addSourcemapLocations( attribute.expression ); + code.insertRight( attribute.expression.start, 'component.' ); + + const usedContexts = new Set(); + attribute.expression.arguments.forEach( arg => { + const contexts = contextualise( code, arg, current.contexts ); + + contexts.forEach( context => { + usedContexts.add( context ); + allUsedContexts.add( context ); + }); + }); + + // 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` ${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' ) ); + if ( updateStatements.length ) current.updateStatements.push( updateStatements.join( '\n' ) ); current.teardownStatements.push( teardownStatements.join( '\n' ) ); current = Object.assign( {}, current, { @@ -211,7 +251,7 @@ export default function generate ( parsed, template ) { } 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' )} export default function createComponent ( options ) { - var component = {}; + var component = ${templateProperties.methods ? `Object.create( template.methods )` : `{}`}; var state = {}; var observers = { @@ -374,13 +414,13 @@ export default function generate ( parsed, template ) { }; 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; } `; - const pattern = /\[✂(\d+)-(\d+)/; + const pattern = /\[✂(\d+)-(\d+)$/; const parts = result.split( '✂]' ); const finalChunk = parts.pop(); @@ -404,7 +444,7 @@ export default function generate ( parsed, template ) { sortedBySource.forEach( part => { code.remove( c, part.start ); - code.insertLeft( part.start, part.chunk ); + code.insertRight( part.start, part.chunk ); c = part.end; }); diff --git a/compiler/generate/utils/contextualise.js b/compiler/generate/utils/contextualise.js new file mode 100644 index 0000000000..8743506a40 --- /dev/null +++ b/compiler/generate/utils/contextualise.js @@ -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; +} diff --git a/test/compiler/custom-method/_config.js b/test/compiler/custom-method/_config.js new file mode 100644 index 0000000000..2931baf74c --- /dev/null +++ b/test/compiler/custom-method/_config.js @@ -0,0 +1,19 @@ +import * as assert from 'assert'; + +export default { + html: '

0

', + 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, '

1

' ); + + button.dispatchEvent( event ); + assert.equal( component.get( 'counter' ), 2 ); + assert.equal( target.innerHTML, '

2

' ); + + assert.equal( component.foo(), 42 ); + } +}; diff --git a/test/compiler/custom-method/main.svelte b/test/compiler/custom-method/main.svelte new file mode 100644 index 0000000000..06ad01ca79 --- /dev/null +++ b/test/compiler/custom-method/main.svelte @@ -0,0 +1,21 @@ + + +

{{counter}}

+ + diff --git a/test/compiler/default-data/_config.js b/test/compiler/default-data/_config.js index aed2eb41bb..14c30650f8 100644 --- a/test/compiler/default-data/_config.js +++ b/test/compiler/default-data/_config.js @@ -1,6 +1,5 @@ import * as assert from 'assert'; export default { - description: 'hello world', html: '

Hello world!

' }; diff --git a/test/compiler/each-block/_config.js b/test/compiler/each-block/_config.js index 27c25a79f2..c5a7041755 100644 --- a/test/compiler/each-block/_config.js +++ b/test/compiler/each-block/_config.js @@ -1,7 +1,6 @@ import * as assert from 'assert'; export default { - description: '{{#each}}...{{/each}} block', data: { animals: [ 'alpaca', 'baboon', 'capybara' ] }, diff --git a/test/compiler/each-blocks-nested-b/_config.js b/test/compiler/each-blocks-nested-b/_config.js index 3485b08e26..e3b5bc3d7e 100644 --- a/test/compiler/each-blocks-nested-b/_config.js +++ b/test/compiler/each-blocks-nested-b/_config.js @@ -1,5 +1,4 @@ export default { - description: 'nested {{#each}} blocks where inner iterates over property of outer', data: { categories: [ { diff --git a/test/compiler/each-blocks-nested/_config.js b/test/compiler/each-blocks-nested/_config.js index 33883ee499..a0f2190346 100644 --- a/test/compiler/each-blocks-nested/_config.js +++ b/test/compiler/each-blocks-nested/_config.js @@ -1,5 +1,4 @@ export default { - description: 'nested {{#each}} blocks', data: { columns: [ 'a', 'b', 'c' ], rows: [ 1, 2, 3 ] diff --git a/test/compiler/event-handler/_config.js b/test/compiler/event-handler/_config.js index f84267932d..b3d6b5e1b0 100644 --- a/test/compiler/event-handler/_config.js +++ b/test/compiler/event-handler/_config.js @@ -1,9 +1,15 @@ import * as assert from 'assert'; export default { - description: 'attaches event handlers', html: '', - test ( component, target ) { - assert.ok( false, 'TODO fire synthetic event to test' ); + test ( component, target, window ) { + const button = target.querySelector( 'button' ); + const event = new window.MouseEvent( 'click' ); + + button.dispatchEvent( event ); + assert.equal( target.innerHTML, '

hello!

' ); + + button.dispatchEvent( event ); + assert.equal( target.innerHTML, '' ); } }; diff --git a/test/compiler/hello-world/_config.js b/test/compiler/hello-world/_config.js index 6c5815a9fd..4c3701271d 100644 --- a/test/compiler/hello-world/_config.js +++ b/test/compiler/hello-world/_config.js @@ -1,7 +1,6 @@ import * as assert from 'assert'; export default { - description: 'hello world', data: { name: 'world' }, diff --git a/test/compiler/if-block/_config.js b/test/compiler/if-block/_config.js index 17ed13cf3d..ba75107b36 100644 --- a/test/compiler/if-block/_config.js +++ b/test/compiler/if-block/_config.js @@ -1,7 +1,6 @@ import * as assert from 'assert'; export default { - description: '{{#if}}...{{/if}} block', data: { visible: true }, diff --git a/test/compiler/single-static-element/_config.js b/test/compiler/single-static-element/_config.js index 59a8ceef8d..2b2f08306c 100644 --- a/test/compiler/single-static-element/_config.js +++ b/test/compiler/single-static-element/_config.js @@ -1,4 +1,3 @@ export default { - description: 'creates a factory with a single static element', html: 'test' }; diff --git a/test/test.js b/test/test.js index afda7eda33..23dc2dafce 100644 --- a/test/test.js +++ b/test/test.js @@ -123,7 +123,7 @@ describe( 'svelte', () => { } if ( config.test ) { - config.test( component, target ); + config.test( component, target, window ); } else { component.teardown(); assert.equal( target.innerHTML, '' );