initialise fragments with data, rather than waiting for first update

pull/31/head
Rich-Harris 8 years ago
parent 7736866397
commit 04388f7a0e

@ -17,11 +17,11 @@ export default function generate ( parsed, source, options ) {
}
renderers.push( deindent`
function ${fragment.name} ( component, target${fragment.useAnchor ? ', anchor' : ''} ) {
function ${fragment.name} ( ${fragment.params}, component, target${fragment.useAnchor ? ', anchor' : ''} ) {
${fragment.initStatements.join( '\n\n' )}
return {
update: function ( ${fragment.params.join( ', ' )} ) {
update: function ( changed, ${fragment.params} ) {
${fragment.updateStatements.join( '\n\n' )}
},
@ -86,7 +86,8 @@ export default function generate ( parsed, source, options ) {
return {
dependencies,
contexts: usedContexts,
snippet: `[✂${expression.start}-${expression.end}✂]`
snippet: `[✂${expression.start}-${expression.end}✂]`,
string: generator.code.slice( expression.start, expression.end )
};
},
@ -211,7 +212,7 @@ export default function generate ( parsed, source, options ) {
contexts: {},
indexes: {},
params: [ 'changed', 'root' ],
params: 'root',
indexNames: {},
listNames: {},
@ -222,12 +223,15 @@ export default function generate ( parsed, source, options ) {
generator.addRenderer( generator.pop() );
const topLevelStatements = [];
const setStatements = [ deindent`
const oldState = state;
state = Object.assign( {}, oldState, newState );
` ];
if ( templateProperties.computed ) {
const statements = [];
const dependencies = new Map();
templateProperties.computed.properties.forEach( prop => {
@ -249,7 +253,7 @@ export default function generate ( parsed, source, options ) {
const deps = dependencies.get( key );
deps.forEach( visit );
setStatements.push( deindent`
statements.push( deindent`
if ( ${deps.map( dep => `( '${dep}' in newState && typeof state.${dep} === 'object' || state.${dep} !== oldState.${dep} )` ).join( ' || ' )} ) {
state.${key} = newState.${key} = template.computed.${key}( ${deps.map( dep => `state.${dep}` ).join( ', ' )} );
}
@ -257,6 +261,14 @@ export default function generate ( parsed, source, options ) {
}
templateProperties.computed.properties.forEach( prop => visit( prop.key.name ) );
topLevelStatements.push( deindent`
function applyComputations ( state, newState, oldState ) {
${statements.join( '\n\n' )}
}
` );
setStatements.push( `applyComputations( state, newState, oldState )` );
}
setStatements.push( deindent`
@ -265,8 +277,6 @@ export default function generate ( parsed, source, options ) {
dispatchObservers( observers.deferred, newState, oldState );
` );
const topLevelStatements = [];
if ( parsed.js ) {
if ( imports.length ) {
topLevelStatements.push( imports.join( '' ).trim() );
@ -290,23 +300,27 @@ export default function generate ( parsed, source, options ) {
}
if ( generator.hasComplexBindings ) {
initStatements.push( `this.__bindings = [];` );
initStatements.push( deindent`
this.__bindings = [];
var mainFragment = renderMainFragment( state, this, options.target );
while ( this.__bindings.length ) this.__bindings.pop()();
` );
setStatements.push( `while ( this.__bindings.length ) this.__bindings.pop()();` );
} else {
initStatements.push( `var mainFragment = renderMainFragment( state, this, options.target );` );
}
initStatements.push( deindent`
var mainFragment = renderMainFragment( this, options.target );
this.set( ${templateProperties.data ? `Object.assign( template.data(), options.data )` : `options.data || {}`} );
` );
if ( templateProperties.onrender ) {
initStatements.push( `template.onrender.call( this );` );
}
const initialState = templateProperties.data ? `Object.assign( template.data(), options.data )` : `options.data || {}`;
topLevelStatements.push( deindent`
export default function ${constructorName} ( options ) {
var component = this;${generator.usesRefs ? `\nthis.refs = {}` : ``}
var state = {};
var state = ${initialState};${templateProperties.computed ? `\napplyComputations( state, state, {} );` : ``}
var observers = {
immediate: Object.create( null ),

@ -9,27 +9,34 @@ export default {
const listName = `${name}_value`;
generator.addSourcemapLocations( node.expression );
const { dependencies, snippet, string } = generator.contextualise( node.expression );
generator.current.initStatements.push( deindent`
var ${name}_anchor = document.createComment( ${JSON.stringify( `#each ${generator.source.slice( node.expression.start, node.expression.end )}` )} );
${generator.current.target}.appendChild( ${name}_anchor );
var ${name}_value = ${snippet};
var ${name}_fragment = document.createDocumentFragment();
var ${name}_iterations = [];
const ${name}_fragment = document.createDocumentFragment();
` );
generator.addSourcemapLocations( node.expression );
for ( var i = 0; i < ${name}_value.length; i += 1 ) {
${name}_iterations[i] = ${renderer}( ${generator.current.params}, ${listName}, ${listName}[i], i, component, ${name}_fragment );
}
const { dependencies, snippet } = generator.contextualise( node.expression );
${name}_anchor.parentNode.insertBefore( ${name}_fragment, ${name}_anchor );
` );
generator.current.updateStatements.push( deindent`
var ${name}_value = ${snippet};
var ${name}_value = ${string};
for ( var i = 0; i < ${name}_value.length; i += 1 ) {
if ( !${name}_iterations[i] ) {
${name}_iterations[i] = ${renderer}( component, ${name}_fragment );
${name}_iterations[i] = ${renderer}( ${generator.current.params}, ${listName}, ${listName}[i], i, component, ${name}_fragment );
} else {
${name}_iterations[i].update( changed, ${generator.current.params}, ${listName}, ${listName}[i], i );
}
const iteration = ${name}_iterations[i];
${name}_iterations[i].update( ${generator.current.params.join( ', ' )}, ${listName}, ${listName}[i], i );
}
for ( var i = ${name}_value.length; i < ${name}_iterations.length; i += 1 ) {
@ -63,7 +70,7 @@ export default {
const contextDependencies = Object.assign( {}, generator.current.contextDependencies );
contextDependencies[ node.context ] = dependencies;
const params = generator.current.params.concat( listName, node.context, indexName );
const params = generator.current.params + `, ${listName}, ${node.context}, ${indexName}`;
generator.current = {
useAnchor: false,

@ -22,13 +22,39 @@ export default {
if ( isComponent ) {
addComponentAttributes( generator, node, local );
if ( local.staticAttributes.length ) {
if ( local.staticAttributes.length || local.dynamicAttributes.length || local.bindings.length ) {
const initialProps = local.staticAttributes
.concat( local.dynamicAttributes )
.map( attribute => `${attribute.name}: ${attribute.value}` );
const statements = [];
if ( initialProps.length ) {
statements.push( deindent`
var ${name}_initialData = {
${initialProps.join( ',\n' )}
};
` );
} else {
statements.push( `var ${name}_initialData = {};` );
}
if ( local.bindings.length ) {
const bindings = local.bindings.map( binding => {
const parts = binding.value.split( '.' );
const tail = parts.pop();
return `if ( '${tail}' in ${parts.join( '.' )} ) ${name}_initialData.${binding.name} = ${binding.value};`;
});
statements.push( bindings.join( '\n' ) );
}
local.init.unshift( deindent`
${statements.join( '\n\n' )}
var ${name} = new template.components.${node.name}({
target: ${generator.current.target},
data: {
${local.staticAttributes.join( ',\n' )}
}
data: ${name}_initialData
});
` );
} else {
@ -62,11 +88,18 @@ export default {
addElementAttributes( generator, node, local );
if ( local.allUsedContexts.size ) {
local.init.push( deindent`
${name}.__svelte = {};
` );
const contextNames = [...local.allUsedContexts];
const initialProps = contextNames.map( contextName => {
if ( contextName === 'root' ) return `root: root`;
const declarations = [...local.allUsedContexts].map( contextName => {
const listName = generator.current.listNames[ contextName ];
const indexName = generator.current.indexNames[ contextName ];
return `${listName}: ${listName},\n${indexName}: ${indexName}`;
}).join( ',\n' );
const updates = contextNames.map( contextName => {
if ( contextName === 'root' ) return `${name}.__svelte.root = root;`;
const listName = generator.current.listNames[ contextName ];
@ -75,7 +108,13 @@ export default {
return `${name}.__svelte.${listName} = ${listName};\n${name}.__svelte.${indexName} = ${indexName};`;
}).join( '\n' );
local.update.push( declarations );
local.init.push( deindent`
${name}.__svelte = {
${initialProps}
};
` );
local.update.push( updates );
}
let render = local.namespace ?

@ -10,18 +10,36 @@ export default {
const elseName = `elseBlock_${i}`;
const elseRenderer = `renderElseBlock_${i}`;
generator.addSourcemapLocations( node.expression );
const { snippet, string } = generator.contextualise( node.expression );
generator.current.initStatements.push( deindent`
var ${name}_anchor = document.createComment( ${JSON.stringify( `#if ${generator.source.slice( node.expression.start, node.expression.end )}` )} );
${generator.current.target}.appendChild( ${name}_anchor );
var ${name} = null;${node.else ? `\nvar ${elseName} = null;` : ``}
` );
generator.addSourcemapLocations( node.expression );
const { snippet } = generator.contextualise( node.expression );
if ( node.else ) {
generator.current.initStatements.push( deindent`
var ${name} = null;
var ${elseName} = null;
if ( ${snippet} ) {
${name} = ${renderer}( ${generator.current.params}, component, ${generator.current.target}, ${name}_anchor );
} else {
${elseName} = ${elseRenderer}( ${generator.current.params}, component, ${generator.current.target}, ${name}_anchor );
}
` );
} else {
generator.current.initStatements.push( deindent`
var ${name} = ${snippet} ? ${renderer}( ${generator.current.params}, component, ${generator.current.target}, ${name}_anchor ) : null;
` );
}
const ifTrue = [ deindent`
if ( !${name } ) {
${name} = ${renderer}( component, ${generator.current.target}, ${name}_anchor );
${name} = ${renderer}( ${generator.current.params}, component, ${generator.current.target}, ${name}_anchor );
} else {
${name}.update( changed, ${generator.current.params} );
}
` ];
@ -44,25 +62,25 @@ export default {
if ( node.else ) {
ifFalse.push( deindent`
if ( !${elseName } ) {
${elseName} = ${elseRenderer}( component, ${generator.current.target}, ${name}_anchor );
${elseName} = ${elseRenderer}( ${generator.current.params}, component, ${generator.current.target}, ${name}_anchor );
} else {
${elseName}.update( changed, ${generator.current.params} );
}
` );
}
let update = deindent`
if ( ${snippet} ) {
if ( ${string} ) {
${ifTrue.join( '\n\n' )}
}
else {
${ifFalse.join( '\n\n' )}
}
if ( ${name} ) ${name}.update( ${generator.current.params.join( ', ' )} );
`;
if ( node.else ) {
update += `\nif ( ${elseName} ) ${elseName}.update( ${generator.current.params.join( ', ' )} );`;
update += `\nif ( ${elseName} ) ${elseName}.update( changed, ${generator.current.params} );`;
}
generator.current.updateStatements.push( update );

@ -5,36 +5,17 @@ export default {
enter ( generator, node ) {
const name = generator.current.counter( 'text' );
const { snippet, string } = generator.contextualise( node.expression );
generator.current.initStatements.push( deindent`
var ${name} = document.createTextNode( '' );
var ${name}_value = '';
var ${name} = document.createTextNode( ${snippet} );
${generator.current.target}.appendChild( ${name} );
` );
generator.addSourcemapLocations( node.expression );
const { contexts, snippet } = generator.contextualise( node.expression );
if ( isReference( node.expression ) ) {
const reference = `${generator.source.slice( node.expression.start, node.expression.end )}`;
const qualified = contexts[0] === 'root' ? `root.${reference}` : reference;
generator.current.updateStatements.push( deindent`
if ( ${snippet} !== ${name}_value ) {
${name}_value = ${qualified};
${name}.data = ${name}_value;
}
` );
} else {
const temp = generator.getName( 'temp' );
generator.current.updateStatements.push( deindent`
var ${temp} = ${snippet};
if ( ${temp} !== ${name}_value ) {
${name}_value = ${temp};
${name}.data = ${name}_value;
}
` );
}
generator.current.updateStatements.push( deindent`
${name}.data = ${string};
` );
}
};

@ -4,12 +4,16 @@ import deindent from '../../utils/deindent.js';
export default function addComponentAttributes ( generator, 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( `${attribute.name}: true` );
local.staticAttributes.push({
name: attribute.name,
value: true
});
}
else if ( attribute.value.length === 1 ) {
@ -18,17 +22,20 @@ export default function addComponentAttributes ( generator, node, local ) {
if ( value.type === 'Text' ) {
// static attributes
const result = isNaN( parseFloat( value.data ) ) ? JSON.stringify( value.data ) : value.data;
local.staticAttributes.push( `${attribute.name}: ${result}` );
local.staticAttributes.push({
name: attribute.name,
value: result
});
}
else {
// simple dynamic attributes
const { dependencies, snippet } = generator.contextualise( value.expression );
const { dependencies, string } = generator.contextualise( value.expression );
// TODO only update attributes that have changed
local.dynamicAttributes.push({
name: attribute.name,
value: snippet,
value: string,
dependencies
});
}
@ -45,12 +52,12 @@ export default function addComponentAttributes ( generator, node, local ) {
} else {
generator.addSourcemapLocations( chunk.expression );
const { dependencies, snippet } = generator.contextualise( chunk.expression );
const { dependencies, string } = generator.contextualise( chunk.expression );
dependencies.forEach( dependency => {
if ( !~allDependencies.indexOf( dependency ) ) allDependencies.push( dependency );
});
return `( ${snippet} )`;
return `( ${string} )`;
}
}).join( ' + ' )
);

@ -61,17 +61,14 @@ export default function addElementAttributes ( generator, node, local ) {
dynamic = true;
// dynamic but potentially non-string attributes
const { snippet } = generator.contextualise( value.expression );
const { snippet, string } = generator.contextualise( value.expression );
if ( propertyName ) {
local.update.push( deindent`
${local.name}.${propertyName} = ${snippet};
` );
} else {
local.update.push( deindent`
${local.name}.setAttribute( '${attribute.name}', ${snippet} );
` );
}
const updater = propertyName ?
`${local.name}.${propertyName} = ${snippet};` :
`${local.name}.setAttribute( '${attribute.name}', ${string} );`; // TODO use snippet both times see note below
local.init.push( updater );
local.update.push( updater );
}
}
@ -85,21 +82,18 @@ export default function addElementAttributes ( generator, node, local ) {
} else {
generator.addSourcemapLocations( chunk.expression );
const { snippet } = generator.contextualise( chunk.expression );
return `( ${snippet} )`;
const { string } = generator.contextualise( chunk.expression ); // TODO use snippet for sourcemap support need to add a 'copy' feature to MagicString first
return `( ${string} )`;
}
}).join( ' + ' )
);
if ( propertyName ) {
local.update.push( deindent`
${local.name}.${propertyName} = ${value};
` );
} else {
local.update.push( deindent`
${local.name}.setAttribute( '${attribute.name}', ${value} );
` );
}
const updater = propertyName ?
`${local.name}.${propertyName} = ${value};` :
`${local.name}.setAttribute( '${attribute.name}', ${value} );`;
local.init.push( updater );
local.update.push( updater );
}
if ( isBoundOptionValue ) {

@ -7,8 +7,16 @@ export default function createBinding ( generator, node, attribute, current, loc
const deep = parts.length > 1;
const contextual = parts[0] in current.contexts;
if ( contextual ) local.allUsedContexts.add( parts[0] );
if ( local.isComponent ) {
local.bindings.push({
name: attribute.name,
value: contextual ? attribute.value : `root.${attribute.value}`
});
}
const handler = current.counter( `${local.name}ChangeHandler` );
let setter;
@ -94,6 +102,8 @@ export default function createBinding ( generator, node, attribute, current, loc
}
` );
} else {
const updateElement = `${local.name}.${attribute.name} = ${contextual ? attribute.value : `root.${attribute.value}`}`;
local.init.push( deindent`
var ${local.name}_updating = false;
@ -104,10 +114,11 @@ export default function createBinding ( generator, node, attribute, current, loc
}
${local.name}.addEventListener( '${eventName}', ${handler}, false );
${updateElement};
` );
local.update.push( deindent`
if ( !${local.name}_updating ) ${local.name}.${attribute.name} = ${contextual ? attribute.value : `root.${attribute.value}`}
if ( !${local.name}_updating ) ${updateElement};
` );
local.teardown.push( deindent`

@ -6,5 +6,6 @@ export default {
{ foo: true, bar: true }
]
},
html: `<div class="foo ">1</div><div class=" bar">2</div><div class="foo bar">3</div><!--#each items-->`
};

@ -1,5 +1,6 @@
export default {
html: '<!--#if visible-->',
html: '',
test ( assert, component, target, window ) {
component.set({ visible: true });
assert.equal( component.refs.input, window.document.activeElement );

@ -6,7 +6,12 @@ export default {
{ description: 'three', completed: false }
]
},
html: `<div><input type="checkbox"><p>one</p></div><div><input type="checkbox"><p>two</p></div><div><input type="checkbox"><p>three</p></div><!--#each items-->\n\n<p>1 completed</p>`,
html: `
<div><input type="checkbox"><p>one</p></div><div><input type="checkbox"><p>two</p></div><div><input type="checkbox"><p>three</p></div>
<p>1 completed</p>
`,
test ( assert, component, target, window ) {
const inputs = [ ...target.querySelectorAll( 'input' ) ];
@ -20,13 +25,19 @@ export default {
inputs[1].dispatchEvent( event );
assert.equal( component.get( 'numCompleted' ), 2 );
assert.equal( target.innerHTML, `<div><input type="checkbox"><p>one</p></div><div><input type="checkbox"><p>two</p></div><div><input type="checkbox"><p>three</p></div><!--#each items-->\n\n<p>2 completed</p>` );
assert.htmlEqual( target.innerHTML, `
<div><input type="checkbox"><p>one</p></div><div><input type="checkbox"><p>two</p></div><div><input type="checkbox"><p>three</p></div>
<p>2 completed</p>
` );
const items = component.get( 'items' );
items[2].completed = true;
component.set({ items });
assert.ok( inputs[2].checked );
assert.equal( target.innerHTML, `<div><input type="checkbox"><p>one</p></div><div><input type="checkbox"><p>two</p></div><div><input type="checkbox"><p>three</p></div><!--#each items-->\n\n<p>3 completed</p>` );
assert.htmlEqual( target.innerHTML, `
<div><input type="checkbox"><p>one</p></div><div><input type="checkbox"><p>two</p></div><div><input type="checkbox"><p>three</p></div>
<p>3 completed</p>
` );
}
};

@ -4,7 +4,9 @@ export default {
name: 'alice'
}
},
html: `<input>\n<p>hello alice</p>`,
test ( assert, component, target, window ) {
const input = target.querySelector( 'input' );

@ -0,0 +1,10 @@
export default {
test ( assert, component, target ) {
component.set({ q: 42 });
component.set({ foo: true });
assert.htmlEqual( target.innerHTML, `
<p>42</p>
` );
}
};

@ -0,0 +1,17 @@
{{#if foo}}
<Widget p='{{q}}'/>
{{/if}}
<script>
import Widget from './Widget.html';
export default {
data: () => ({
foo: false
}),
components: {
Widget
}
};
</script>

@ -2,7 +2,9 @@ export default {
data: {
visible: true
},
html: '<div><!--#if visible--><p>i am a widget</p></div>', // TODO comment should follow component...
html: '<div><p>i am a widget</p></div>',
test ( assert, component ) {
let count = 0;

@ -5,6 +5,8 @@ export default {
component.set({ currentFilter: 'all' });
assert.equal( target.innerHTML, `<ul><li>one</li><!--#if filter(item, currentFilter)--><li>two</li><!--#if filter(item, currentFilter)--><li>three</li><!--#if filter(item, currentFilter)--><!--#each items--></ul>` );
assert.htmlEqual( target.innerHTML, `
<ul><li>one</li><li>two</li><li>three</li></ul>`
);
}
};

@ -1,9 +1,13 @@
export default {
solo: true,
data: {
columns: [ 'a', 'b', 'c' ],
rows: [ 1, 2, 3 ]
},
html: `<div>a, 1</div><div>a, 2</div><div>a, 3</div><!--#each rows--><div>b, 1</div><div>b, 2</div><div>b, 3</div><!--#each rows--><div>c, 1</div><div>c, 2</div><div>c, 3</div><!--#each rows--><!--#each columns-->`,
test ( assert, component, target ) {
// TODO
}

Loading…
Cancel
Save