component two-way bindings

pull/31/head
Rich-Harris 8 years ago
parent 22751e9ba4
commit cf1a80a28f

@ -7,7 +7,7 @@ import flattenReference from './utils/flattenReference.js';
import visitors from './visitors/index.js'; import visitors from './visitors/index.js';
import processCss from './css/process.js'; import processCss from './css/process.js';
export default function generate ( parsed, source, options = {} ) { export default function generate ( parsed, source, options ) {
const renderers = []; const renderers = [];
const generator = { const generator = {
@ -255,12 +255,10 @@ export default function generate ( parsed, source, options = {} ) {
setStatements.push( deindent` setStatements.push( deindent`
dispatchObservers( observers.immediate, newState, oldState ); dispatchObservers( observers.immediate, newState, oldState );
mainFragment.update( state ); if ( mainFragment ) mainFragment.update( state );
dispatchObservers( observers.deferred, newState, oldState ); dispatchObservers( observers.deferred, newState, oldState );
` ); ` );
const constructorName = options.name || 'SvelteComponent';
const topLevelStatements = []; const topLevelStatements = [];
if ( parsed.js ) { if ( parsed.js ) {
@ -277,6 +275,8 @@ export default function generate ( parsed, source, options = {} ) {
topLevelStatements.push( ...renderers.reverse() ); topLevelStatements.push( ...renderers.reverse() );
const constructorName = options.name || 'SvelteComponent';
topLevelStatements.push( deindent` topLevelStatements.push( deindent`
export default function ${constructorName} ( options ) { export default function ${constructorName} ( options ) {
var component = this;${generator.usesRefs ? `\nthis.refs = {}` : ``} var component = this;${generator.usesRefs ? `\nthis.refs = {}` : ``}
@ -351,16 +351,16 @@ export default function generate ( parsed, source, options = {} ) {
}; };
this.teardown = function teardown () { this.teardown = function teardown () {
this.fire( 'teardown' );${templateProperties.onteardown ? `\ntemplate.onteardown.call( this );` : ``}
mainFragment.teardown(); mainFragment.teardown();
mainFragment = null; mainFragment = null;
state = {}; state = {};
this.fire( 'teardown' );${templateProperties.onteardown ? `\ntemplate.onteardown.call( this );` : ``}
}; };
${parsed.css ? `if ( !addedCss ) addCss();` : ''} ${parsed.css ? `if ( !addedCss ) addCss();` : ''}
let mainFragment = renderMainFragment( this, options.target ); var mainFragment = renderMainFragment( this, options.target );
this.set( ${templateProperties.data ? `Object.assign( template.data(), options.data )` : `options.data`} ); this.set( ${templateProperties.data ? `Object.assign( template.data(), options.data )` : `options.data`} );
${templateProperties.onrender ? `template.onrender.call( this );` : ``} ${templateProperties.onrender ? `template.onrender.call( this );` : ``}

@ -10,6 +10,8 @@ export default {
const local = { const local = {
name, name,
namespace: name === 'svg' ? 'http://www.w3.org/2000/svg' : generator.current.namespace, namespace: name === 'svg' ? 'http://www.w3.org/2000/svg' : generator.current.namespace,
isComponent,
allUsedContexts: new Set(), allUsedContexts: new Set(),
init: [], init: [],

@ -1,3 +1,4 @@
import createBinding from './binding/index.js';
import deindent from '../../utils/deindent.js'; import deindent from '../../utils/deindent.js';
export default function addComponentAttributes ( generator, node, local ) { export default function addComponentAttributes ( generator, node, local ) {
@ -85,7 +86,7 @@ export default function addComponentAttributes ( generator, node, local ) {
} }
else if ( attribute.type === 'Binding' ) { else if ( attribute.type === 'Binding' ) {
throw new Error( 'TODO component bindings' ); createBinding( node, attribute, generator.current, local );
} }
else if ( attribute.type === 'Ref' ) { else if ( attribute.type === 'Ref' ) {

@ -149,7 +149,7 @@ export default function addElementAttributes ( generator, node, local ) {
} }
else if ( attribute.type === 'Binding' ) { else if ( attribute.type === 'Binding' ) {
createBinding( node, local.name, attribute, generator.current, local.init, local.update, local.teardown, local.allUsedContexts ); createBinding( node, attribute, generator.current, local );
} }
else if ( attribute.type === 'Ref' ) { else if ( attribute.type === 'Ref' ) {

@ -2,14 +2,14 @@ import deindent from '../../../utils/deindent.js';
import isReference from '../../../utils/isReference.js'; import isReference from '../../../utils/isReference.js';
import flattenReference from '../../../utils/flattenReference.js'; import flattenReference from '../../../utils/flattenReference.js';
export default function createBinding ( node, name, attribute, current, initStatements, updateStatements, teardownStatements, allUsedContexts ) { export default function createBinding ( node, attribute, current, local ) {
const parts = attribute.value.split( '.' ); const parts = attribute.value.split( '.' );
const deep = parts.length > 1; const deep = parts.length > 1;
const contextual = parts[0] in current.contexts; const contextual = parts[0] in current.contexts;
if ( contextual ) allUsedContexts.add( parts[0] ); if ( contextual ) local.allUsedContexts.add( parts[0] );
const handler = current.counter( `${name}ChangeHandler` ); const handler = current.counter( `${local.name}ChangeHandler` );
let setter; let setter;
let eventName = 'change'; let eventName = 'change';
@ -54,26 +54,43 @@ export default function createBinding ( node, name, attribute, current, initStat
component.set({ ${parts[0]}: ${parts[0]} }); component.set({ ${parts[0]}: ${parts[0]} });
`; `;
} else { } else {
setter = `component.set({ ${attribute.value}: ${name}.${attribute.name} });`; const value = local.isComponent ? `value` : `${local.name}.${attribute.name}`;
setter = `component.set({ ${attribute.value}: ${value} });`;
} }
initStatements.push( deindent` if ( local.isComponent ) {
var ${name}_updating = false; local.init.push( deindent`
var ${local.name}_updating = false;
function ${handler} () { ${local.name}.observe( '${attribute.name}', function ( value ) {
${name}_updating = true; ${local.name}_updating = true;
${setter} ${setter}
${name}_updating = false; ${local.name}_updating = false;
} });
` );
local.update.push( deindent`
if ( !${local.name}_updating ) ${local.name}.set({ ${attribute.name}: ${contextual ? attribute.value : `root.${attribute.value}`} });
` );
} else {
local.init.push( deindent`
var ${local.name}_updating = false;
${name}.addEventListener( '${eventName}', ${handler}, false ); function ${handler} () {
` ); ${local.name}_updating = true;
${setter}
${local.name}_updating = false;
}
${local.name}.addEventListener( '${eventName}', ${handler}, false );
` );
updateStatements.push( deindent` local.update.push( deindent`
if ( !${name}_updating ) ${name}.${attribute.name} = ${contextual ? attribute.value : `root.${attribute.value}`} if ( !${local.name}_updating ) ${local.name}.${attribute.name} = ${contextual ? attribute.value : `root.${attribute.value}`}
` ); ` );
teardownStatements.push( deindent` local.teardown.push( deindent`
${name}.removeEventListener( '${eventName}', ${handler}, false ); ${local.name}.removeEventListener( '${eventName}', ${handler}, false );
` ); ` );
}
} }

@ -1,10 +1,10 @@
import parse from './parse/index.js'; import parse from './parse/index.js';
import generate from './generate/index.js'; import generate from './generate/index.js';
export function compile ( template ) { export function compile ( template, options = {} ) {
const parsed = parse( template ); const parsed = parse( template, options );
// TODO validate template // TODO validate template
const generated = generate( parsed, template ); const generated = generate( parsed, template, options );
return generated; return generated;
} }

@ -0,0 +1,9 @@
<button on:click='set({ count: count + 1 })'>+1</button>
<script>
export default {
data: () => ({
count: 0
})
};
</script>

@ -0,0 +1,27 @@
export default {
html: `
<button>+1</button>
<p>count: 0</p>
`,
test ( assert, component, target, window ) {
const click = new window.MouseEvent( 'click' );
const button = target.querySelector( 'button' );
button.dispatchEvent( click );
assert.equal( component.get( 'x' ), 1 );
assert.htmlEqual( target.innerHTML, `
<button>+1</button>
<p>count: 1</p>
` );
button.dispatchEvent( click );
assert.equal( component.get( 'x' ), 2 );
assert.htmlEqual( target.innerHTML, `
<button>+1</button>
<p>count: 2</p>
` );
}
};

@ -0,0 +1,12 @@
<Counter bind:count='x'/>
<p>count: {{x}}</p>
<script>
import Counter from './Counter.html';
export default {
components: {
Counter
}
};
</script>

@ -71,12 +71,12 @@ describe( 'svelte', () => {
} }
if ( child.nodeType === 3 ) { if ( child.nodeType === 3 ) {
child.data = child.data.replace( /\s{2,}/, ' ' ); child.data = child.data.replace( /\s{2,}/, '\n' );
// text // text
if ( previous && previous.nodeType === 3 ) { if ( previous && previous.nodeType === 3 ) {
previous.data += child.data; previous.data += child.data;
previous.data = previous.data.replace( /\s{2,}/, ' ' ); previous.data = previous.data.replace( /\s{2,}/, '\n' );
node.removeChild( child ); node.removeChild( child );
} }
@ -113,8 +113,6 @@ describe( 'svelte', () => {
assert.deepEqual( actual, expected, message ); assert.deepEqual( actual, expected, message );
}; };
assert.htmlEqual( ' <p> foo</p> ', '<p>foo</p>' );
}); });
}); });

Loading…
Cancel
Save