Merge pull request #649 from sveltejs/hydration

[WIP] Hydration
pull/7738/head
Rich Harris 8 years ago committed by GitHub
commit f2e30c06c5

@ -40,7 +40,10 @@ export default class Block {
listName: string; listName: string;
builders: { builders: {
init: CodeBuilder;
create: CodeBuilder; create: CodeBuilder;
claim: CodeBuilder;
hydrate: CodeBuilder;
mount: CodeBuilder; mount: CodeBuilder;
intro: CodeBuilder; intro: CodeBuilder;
update: CodeBuilder; update: CodeBuilder;
@ -86,7 +89,10 @@ export default class Block {
this.listName = options.listName; this.listName = options.listName;
this.builders = { this.builders = {
init: new CodeBuilder(),
create: new CodeBuilder(), create: new CodeBuilder(),
claim: new CodeBuilder(),
hydrate: new CodeBuilder(),
mount: new CodeBuilder(), mount: new CodeBuilder(),
intro: new CodeBuilder(), intro: new CodeBuilder(),
update: new CodeBuilder(), update: new CodeBuilder(),
@ -111,7 +117,7 @@ export default class Block {
this.hasUpdateMethod = false; // determined later this.hasUpdateMethod = false; // determined later
} }
addDependencies(dependencies) { addDependencies(dependencies: string[]) {
dependencies.forEach(dependency => { dependencies.forEach(dependency => {
this.dependencies.add(dependency); this.dependencies.add(dependency);
}); });
@ -120,21 +126,17 @@ export default class Block {
addElement( addElement(
name: string, name: string,
renderStatement: string, renderStatement: string,
claimStatement: string,
parentNode: string, parentNode: string,
needsIdentifier = false needsIdentifier = false
) { ) {
const isToplevel = !parentNode; const isToplevel = !parentNode;
if (needsIdentifier || isToplevel) {
this.builders.create.addLine(`var ${name} = ${renderStatement};`);
this.mount(name, parentNode); this.addVariable(name);
} else { this.builders.create.addLine(`${name} = ${renderStatement};`);
this.builders.create.addLine( this.builders.claim.addLine(`${name} = ${claimStatement};`)
`${this.generator.helper(
'appendNode' this.mount(name, parentNode);
)}( ${renderStatement}, ${parentNode} );`
);
}
if (isToplevel) { if (isToplevel) {
this.builders.unmount.addLine( this.builders.unmount.addLine(
@ -184,7 +186,7 @@ export default class Block {
mount(name: string, parentNode: string) { mount(name: string, parentNode: string) {
if (parentNode) { if (parentNode) {
this.builders.create.addLine( this.builders.mount.addLine(
`${this.generator.helper('appendNode')}( ${name}, ${parentNode} );` `${this.generator.helper('appendNode')}( ${name}, ${parentNode} );`
); );
} else { } else {
@ -210,19 +212,8 @@ export default class Block {
this.addVariable(outroing); this.addVariable(outroing);
} }
if (this.variables.size) {
const variables = Array.from(this.variables.keys())
.map(key => {
const init = this.variables.get(key);
return init !== undefined ? `${key} = ${init}` : key;
})
.join(', ');
this.builders.create.addBlockAtStart(`var ${variables};`);
}
if (this.autofocus) { if (this.autofocus) {
this.builders.create.addLine(`${this.autofocus}.focus();`); this.builders.mount.addLine(`${this.autofocus}.focus();`);
} }
// minor hack we need to ensure that any {{{triples}}} are detached first // minor hack we need to ensure that any {{{triples}}} are detached first
@ -237,7 +228,40 @@ export default class Block {
} }
if (this.first) { if (this.first) {
properties.addBlock(`first: ${this.first},`); properties.addBlock(`first: null,`);
this.builders.hydrate.addLine( `this.first = ${this.first};` );
}
if (this.builders.create.isEmpty()) {
properties.addBlock(`create: ${this.generator.helper('noop')},`);
} else {
properties.addBlock(deindent`
create: function () {
${this.builders.create}
${!this.builders.hydrate.isEmpty() && `this.hydrate();`}
},
`);
}
if (this.generator.hydratable) {
if (this.builders.claim.isEmpty()) {
properties.addBlock(`claim: ${this.generator.helper('noop')},`);
} else {
properties.addBlock(deindent`
claim: function ( nodes ) {
${this.builders.claim}
${!this.builders.hydrate.isEmpty() && `this.hydrate();`}
},
`);
}
}
if (!this.builders.hydrate.isEmpty()) {
properties.addBlock(deindent`
hydrate: function ( nodes ) {
${this.builders.hydrate}
},
`);
} }
if (this.builders.mount.isEmpty()) { if (this.builders.mount.isEmpty()) {
@ -331,7 +355,13 @@ export default class Block {
.key .key
? `, ${localKey}` ? `, ${localKey}`
: ''} ) { : ''} ) {
${this.builders.create} ${this.variables.size > 0 && (
`var ${Array.from(this.variables.keys()).map(key => {
const init = this.variables.get(key);
return init !== undefined ? `${key} = ${init}` : key;
}).join(', ')};`)}
${!this.builders.init.isEmpty() && this.builders.init}
return { return {
${properties} ${properties}

@ -18,6 +18,8 @@ export class DomGenerator extends Generator {
readonly: Set<string>; readonly: Set<string>;
metaBindings: string[]; metaBindings: string[];
hydratable: boolean;
hasIntroTransitions: boolean; hasIntroTransitions: boolean;
hasOutroTransitions: boolean; hasOutroTransitions: boolean;
hasComplexBindings: boolean; hasComplexBindings: boolean;
@ -34,6 +36,8 @@ export class DomGenerator extends Generator {
this.readonly = new Set(); this.readonly = new Set();
this.hydratable = options.hydratable;
// initial values for e.g. window.innerWidth, if there's a <:Window> meta tag // initial values for e.g. window.innerWidth, if there's a <:Window> meta tag
this.metaBindings = []; this.metaBindings = [];
} }
@ -234,7 +238,20 @@ export default function dom(
this._fragment = ${generator.alias( this._fragment = ${generator.alias(
'create_main_fragment' 'create_main_fragment'
)}( this._state, this ); )}( this._state, this );
if ( options.target ) this._fragment.mount( options.target, null );
if ( options.target ) {
${generator.hydratable ?
deindent`
var nodes = ${generator.helper('children')}( options.target );
options.hydrate ? this._fragment.claim( nodes ) : this._fragment.create();
nodes.forEach( ${generator.helper('detachNode')} );
` :
deindent`
this._fragment.create();
`}
this._fragment.mount( options.target, null );
}
${generator.hasComplexBindings && ${generator.hasComplexBindings &&
`while ( this._bindings.length ) this._bindings.pop()();`} `while ( this._bindings.length ) this._bindings.pop()();`}
${(generator.hasComponents || generator.hasIntroTransitions) && ${(generator.hasComponents || generator.hasIntroTransitions) &&

@ -2,6 +2,7 @@ export interface State {
name: string; name: string;
namespace: string; namespace: string;
parentNode: string; parentNode: string;
parentNodes: string;
isTopLevel: boolean; isTopLevel: boolean;
parentNodeName?: string; parentNodeName?: string;
basename?: string; basename?: string;

@ -12,7 +12,7 @@ function isElseIf(node: Node) {
} }
function getChildState(parent: State, child = {}) { function getChildState(parent: State, child = {}) {
return assign({}, parent, { name: null, parentNode: null }, child || {}); return assign({}, parent, { name: null, parentNode: null, parentNodes: 'nodes' }, child || {});
} }
// Whitespace inside one of these elements will not result in // Whitespace inside one of these elements will not result in
@ -285,6 +285,7 @@ const preprocessors = {
isTopLevel: false, isTopLevel: false,
name, name,
parentNode: name, parentNode: name,
parentNodes: block.getUniqueName(`${name}_nodes`),
parentNodeName: node.name, parentNodeName: node.name,
namespace: node.name === 'svg' namespace: node.name === 'svg'
? 'http://www.w3.org/2000/svg' ? 'http://www.w3.org/2000/svg'
@ -398,6 +399,7 @@ export default function preprocess(
const state: State = { const state: State = {
namespace, namespace,
parentNode: null, parentNode: null,
parentNodes: 'nodes',
isTopLevel: true, isTopLevel: true,
}; };

@ -113,7 +113,6 @@ export default function visitComponent(
} }
const componentInitProperties = [ const componentInitProperties = [
`target: ${!isTopLevel ? state.parentNode : 'null'}`,
`_root: ${block.component}._root`, `_root: ${block.component}._root`,
]; ];
@ -123,16 +122,24 @@ export default function visitComponent(
const childBlock = node._block; const childBlock = node._block;
node.children.forEach(child => { node.children.forEach((child: Node) => {
visit(generator, childBlock, childState, child); visit(generator, childBlock, childState, child);
}); });
const yieldFragment = block.getUniqueName(`${name}_yield_fragment`); const yieldFragment = block.getUniqueName(`${name}_yield_fragment`);
block.builders.create.addLine( block.builders.init.addLine(
`var ${yieldFragment} = ${childBlock.name}( ${params}, ${block.component} );` `var ${yieldFragment} = ${childBlock.name}( ${params}, ${block.component} );`
); );
block.builders.create.addLine(
`${yieldFragment}.create();`
);
block.builders.claim.addLine(
`${yieldFragment}.claim( ${state.parentNodes} );`
);
if (childBlock.hasUpdateMethod) { if (childBlock.hasUpdateMethod) {
block.builders.update.addLine( block.builders.update.addLine(
`${yieldFragment}.update( changed, ${params} );` `${yieldFragment}.update( changed, ${params} );`
@ -186,12 +193,6 @@ export default function visitComponent(
}); });
`); `);
if (isTopLevel) {
block.builders.mount.addLine(
`${name}._fragment.mount( ${block.target}, anchor );`
);
}
if (local.dynamicAttributes.length) { if (local.dynamicAttributes.length) {
const updates = local.dynamicAttributes.map(attribute => { const updates = local.dynamicAttributes.map(attribute => {
if (attribute.dependencies.length) { if (attribute.dependencies.length) {
@ -222,6 +223,14 @@ export default function visitComponent(
block.builders.unmount.addLine(`${name}._fragment.unmount();`); block.builders.unmount.addLine(`${name}._fragment.unmount();`);
block.builders.destroy.addLine(`${name}.destroy( false );`); block.builders.destroy.addLine(`${name}.destroy( false );`);
block.builders.create.addBlock(local.create); block.builders.init.addBlock(local.create);
const targetNode = state.parentNode || block.target;
const anchorNode = state.parentNode ? 'null' : 'anchor';
block.builders.create.addLine(`${name}._fragment.create();`);
block.builders.claim.addLine(`${name}._fragment.claim( ${state.parentNodes} );`);
block.builders.mount.addLine(`${name}._fragment.mount( ${targetNode}, ${anchorNode} );` );
if (!local.update.isEmpty()) block.builders.update.addBlock(local.update); if (!local.update.isEmpty()) block.builders.update.addBlock(local.update);
} }

@ -35,7 +35,7 @@ export default function visitEachBlock(
const { snippet } = block.contextualise(node.expression); const { snippet } = block.contextualise(node.expression);
block.builders.create.addLine(`var ${each_block_value} = ${snippet};`); block.builders.init.addLine(`var ${each_block_value} = ${snippet};`);
if (node.key) { if (node.key) {
keyed(generator, block, state, node, snippet, vars); keyed(generator, block, state, node, snippet, vars);
@ -49,6 +49,7 @@ export default function visitEachBlock(
block.addElement( block.addElement(
anchor, anchor,
`${generator.helper('createComment')}()`, `${generator.helper('createComment')}()`,
`${generator.helper('createComment')}()`,
state.parentNode, state.parentNode,
true true
); );
@ -59,23 +60,18 @@ export default function visitEachBlock(
if (node.else) { if (node.else) {
const each_block_else = generator.getUniqueName(`${each_block}_else`); const each_block_else = generator.getUniqueName(`${each_block}_else`);
block.builders.create.addLine(`var ${each_block_else} = null;`); block.builders.init.addLine(`var ${each_block_else} = null;`);
// TODO neaten this up... will end up with an empty line in the block // TODO neaten this up... will end up with an empty line in the block
block.builders.create.addBlock(deindent` block.builders.init.addBlock(deindent`
if ( !${each_block_value}.length ) { if ( !${each_block_value}.length ) {
${each_block_else} = ${node.else._block ${each_block_else} = ${node.else._block.name}( ${params}, ${block.component} );
.name}( ${params}, ${block.component} );
${!isToplevel
? `${each_block_else}.${mountOrIntro}( ${state.parentNode}, null );`
: ''}
} }
`); `);
block.builders.mount.addBlock(deindent` block.builders.mount.addBlock(deindent`
if ( ${each_block_else} ) { if ( ${each_block_else} ) {
${each_block_else}.${mountOrIntro}( ${state.parentNode || ${each_block_else}.${mountOrIntro}( ${state.parentNode || block.target}, null );
block.target}, null );
} }
`); `);
@ -86,8 +82,8 @@ export default function visitEachBlock(
if ( !${each_block_value}.length && ${each_block_else} ) { if ( !${each_block_value}.length && ${each_block_else} ) {
${each_block_else}.update( changed, ${params} ); ${each_block_else}.update( changed, ${params} );
} else if ( !${each_block_value}.length ) { } else if ( !${each_block_value}.length ) {
${each_block_else} = ${node.else._block ${each_block_else} = ${node.else._block.name}( ${params}, ${block.component} );
.name}( ${params}, ${block.component} ); ${each_block_else}.create();
${each_block_else}.${mountOrIntro}( ${parentNode}, ${anchor} ); ${each_block_else}.${mountOrIntro}( ${parentNode}, ${anchor} );
} else if ( ${each_block_else} ) { } else if ( ${each_block_else} ) {
${each_block_else}.unmount(); ${each_block_else}.unmount();
@ -104,8 +100,8 @@ export default function visitEachBlock(
${each_block_else} = null; ${each_block_else} = null;
} }
} else if ( !${each_block_else} ) { } else if ( !${each_block_else} ) {
${each_block_else} = ${node.else._block ${each_block_else} = ${node.else._block.name}( ${params}, ${block.component} );
.name}( ${params}, ${block.component} ); ${each_block_else}.create();
${each_block_else}.${mountOrIntro}( ${parentNode}, ${anchor} ); ${each_block_else}.${mountOrIntro}( ${parentNode}, ${anchor} );
} }
`); `);
@ -136,7 +132,7 @@ function keyed(
block: Block, block: Block,
state: State, state: State,
node: Node, node: Node,
snippet, snippet: string,
{ {
each_block, each_block,
create_each_block, create_each_block,
@ -154,6 +150,10 @@ function keyed(
const last = block.getUniqueName(`${each_block}_last`); const last = block.getUniqueName(`${each_block}_last`);
const expected = block.getUniqueName(`${each_block}_expected`); const expected = block.getUniqueName(`${each_block}_expected`);
block.addVariable(lookup, `Object.create( null )`);
block.addVariable(head);
block.addVariable(last);
if (node.children[0] && node.children[0].type === 'Element') { if (node.children[0] && node.children[0].type === 'Element') {
// TODO or text/tag/raw // TODO or text/tag/raw
node._block.first = node.children[0]._state.parentNode; // TODO this is highly confusing node._block.first = node.children[0]._state.parentNode; // TODO this is highly confusing
@ -162,22 +162,16 @@ function keyed(
node._block.addElement( node._block.addElement(
node._block.first, node._block.first,
`${generator.helper('createComment')}()`, `${generator.helper('createComment')}()`,
`${generator.helper('createComment')}()`,
null, null,
true true
); );
} }
block.builders.create.addBlock(deindent` block.builders.init.addBlock(deindent`
var ${lookup} = Object.create( null );
var ${head};
var ${last};
for ( var ${i} = 0; ${i} < ${each_block_value}.length; ${i} += 1 ) { for ( var ${i} = 0; ${i} < ${each_block_value}.length; ${i} += 1 ) {
var ${key} = ${each_block_value}[${i}].${node.key}; var ${key} = ${each_block_value}[${i}].${node.key};
var ${iteration} = ${lookup}[${key}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component}, ${key} ); var ${iteration} = ${lookup}[${key}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component}, ${key} );
${state.parentNode &&
`${iteration}.${mountOrIntro}( ${state.parentNode}, null );`}
if ( ${last} ) ${last}.next = ${iteration}; if ( ${last} ) ${last}.next = ${iteration};
${iteration}.last = ${last}; ${iteration}.last = ${last};
@ -187,15 +181,32 @@ function keyed(
} }
`); `);
if (!state.parentNode) { const targetNode = state.parentNode || block.target;
block.builders.mount.addBlock(deindent` const anchorNode = state.parentNode ? 'null' : 'anchor';
var ${iteration} = ${head};
while ( ${iteration} ) { block.builders.create.addBlock(deindent`
${iteration}.${mountOrIntro}( ${block.target}, anchor ); var ${iteration} = ${head};
${iteration} = ${iteration}.next; while ( ${iteration} ) {
} ${iteration}.create();
`); ${iteration} = ${iteration}.next;
} }
`);
block.builders.claim.addBlock(deindent`
var ${iteration} = ${head};
while ( ${iteration} ) {
${iteration}.claim( ${state.parentNodes} );
${iteration} = ${iteration}.next;
}
`);
block.builders.mount.addBlock(deindent`
var ${iteration} = ${head};
while ( ${iteration} ) {
${iteration}.${mountOrIntro}( ${targetNode}, ${anchorNode} );
${iteration} = ${iteration}.next;
}
`);
const dynamic = node._block.hasUpdateMethod; const dynamic = node._block.hasUpdateMethod;
const parentNode = state.parentNode || `${anchor}.parentNode`; const parentNode = state.parentNode || `${anchor}.parentNode`;
@ -203,7 +214,7 @@ function keyed(
let destroy; let destroy;
if (node._block.hasOutroMethod) { if (node._block.hasOutroMethod) {
const fn = block.getUniqueName(`${each_block}_outro`); const fn = block.getUniqueName(`${each_block}_outro`);
block.builders.create.addBlock(deindent` block.builders.init.addBlock(deindent`
function ${fn} ( iteration ) { function ${fn} ( iteration ) {
iteration.outro( function () { iteration.outro( function () {
iteration.unmount(); iteration.unmount();
@ -227,7 +238,7 @@ function keyed(
`; `;
} else { } else {
const fn = block.getUniqueName(`${each_block}_destroy`); const fn = block.getUniqueName(`${each_block}_destroy`);
block.builders.create.addBlock(deindent` block.builders.init.addBlock(deindent`
function ${fn} ( iteration ) { function ${fn} ( iteration ) {
iteration.unmount(); iteration.unmount();
iteration.destroy(); iteration.destroy();
@ -271,7 +282,6 @@ function keyed(
} else { } else {
if ( ${iteration} ) { if ( ${iteration} ) {
// probably a deletion // probably a deletion
while ( ${expected} && ${expected}.key !== ${key} ) { while ( ${expected} && ${expected}.key !== ${key} ) {
${expected}.discard = true; ${expected}.discard = true;
discard_pile.push( ${expected} ); discard_pile.push( ${expected} );
@ -286,6 +296,7 @@ function keyed(
} else { } else {
// key is being inserted // key is being inserted
${iteration} = ${lookup}[${key}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component}, ${key} ); ${iteration} = ${lookup}[${key}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component}, ${key} );
${iteration}.create();
${iteration}.${mountOrIntro}( ${parentNode}, ${expected}.first ); ${iteration}.${mountOrIntro}( ${parentNode}, ${expected}.first );
${expected}.last = ${iteration}; ${expected}.last = ${iteration};
@ -300,6 +311,7 @@ function keyed(
${iteration}.mount( ${parentNode}, ${anchor} ); ${iteration}.mount( ${parentNode}, ${anchor} );
} else { } else {
${iteration} = ${lookup}[${key}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component}, ${key} ); ${iteration} = ${lookup}[${key}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component}, ${key} );
${iteration}.create();
${iteration}.${mountOrIntro}( ${parentNode}, ${anchor} ); ${iteration}.${mountOrIntro}( ${parentNode}, ${anchor} );
} }
} }
@ -353,27 +365,38 @@ function unkeyed(
mountOrIntro, mountOrIntro,
} }
) { ) {
block.builders.create.addBlock(deindent` block.builders.init.addBlock(deindent`
var ${iterations} = []; var ${iterations} = [];
for ( var ${i} = 0; ${i} < ${each_block_value}.length; ${i} += 1 ) { for ( var ${i} = 0; ${i} < ${each_block_value}.length; ${i} += 1 ) {
${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} ); ${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} );
${state.parentNode &&
`${iterations}[${i}].${mountOrIntro}( ${state.parentNode}, null );`}
} }
`); `);
if (!state.parentNode) { const targetNode = state.parentNode || block.target;
block.builders.mount.addBlock(deindent` const anchorNode = state.parentNode ? 'null' : 'anchor';
for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) {
${iterations}[${i}].${mountOrIntro}( ${block.target}, anchor ); block.builders.create.addBlock(deindent`
} for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) {
`); ${iterations}[${i}].create();
} }
`);
block.builders.claim.addBlock(deindent`
for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) {
${iterations}[${i}].claim( ${state.parentNodes} );
}
`);
block.builders.mount.addBlock(deindent`
for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) {
${iterations}[${i}].${mountOrIntro}( ${targetNode}, ${anchorNode} );
}
`);
const dependencies = block.findDependencies(node.expression); const dependencies = block.findDependencies(node.expression);
const allDependencies = new Set(node._block.dependencies); const allDependencies = new Set(node._block.dependencies);
dependencies.forEach(dependency => { dependencies.forEach((dependency: string) => {
allDependencies.add(dependency); allDependencies.add(dependency);
}); });
@ -392,6 +415,7 @@ function unkeyed(
${iterations}[${i}].update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} ); ${iterations}[${i}].update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} );
} else { } else {
${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} ); ${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} );
${iterations}[${i}].create();
} }
${iterations}[${i}].intro( ${parentNode}, ${anchor} ); ${iterations}[${i}].intro( ${parentNode}, ${anchor} );
` `
@ -400,6 +424,7 @@ function unkeyed(
${iterations}[${i}].update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} ); ${iterations}[${i}].update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} );
} else { } else {
${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} ); ${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} );
${iterations}[${i}].create();
${iterations}[${i}].mount( ${parentNode}, ${anchor} ); ${iterations}[${i}].mount( ${parentNode}, ${anchor} );
} }
` `

@ -22,7 +22,11 @@ export default function visitAttribute(
const isIndirectlyBoundValue = const isIndirectlyBoundValue =
name === 'value' && name === 'value' &&
(node.name === 'option' || // TODO check it's actually bound (node.name === 'option' || // TODO check it's actually bound
(node.name === 'input' && node.attributes.find((attribute: Node) => attribute.type === 'Binding' && /checked|group/.test(attribute.name)))); (node.name === 'input' &&
node.attributes.find(
(attribute: Node) =>
attribute.type === 'Binding' && /checked|group/.test(attribute.name)
)));
const propertyName = isIndirectlyBoundValue const propertyName = isIndirectlyBoundValue
? '__value' ? '__value'
@ -76,7 +80,7 @@ export default function visitAttribute(
// annoying special case // annoying special case
const isMultipleSelect = const isMultipleSelect =
node.name === 'select' && node.name === 'select' &&
node.attributes.find(attr => attr.name.toLowerCase() === 'multiple'); // TODO use getStaticAttributeValue node.attributes.find((attr: Node) => attr.name.toLowerCase() === 'multiple'); // TODO use getStaticAttributeValue
const i = block.getUniqueName('i'); const i = block.getUniqueName('i');
const option = block.getUniqueName('option'); const option = block.getUniqueName('option');
@ -97,17 +101,17 @@ export default function visitAttribute(
} }
`; `;
block.builders.create.addLine(deindent` block.builders.hydrate.addLine(deindent`
${last} = ${value} ${last} = ${value}
${updater} ${updater}
`); `);
} else if (propertyName) { } else if (propertyName) {
block.builders.create.addLine( block.builders.hydrate.addLine(
`${state.parentNode}.${propertyName} = ${last} = ${value};` `${state.parentNode}.${propertyName} = ${last} = ${value};`
); );
updater = `${state.parentNode}.${propertyName} = ${last};`; updater = `${state.parentNode}.${propertyName} = ${last};`;
} else { } else {
block.builders.create.addLine( block.builders.hydrate.addLine(
`${generator.helper( `${generator.helper(
method method
)}( ${state.parentNode}, '${name}', ${last} = ${value} );` )}( ${state.parentNode}, '${name}', ${last} = ${value} );`
@ -132,10 +136,10 @@ export default function visitAttribute(
const statement = propertyName const statement = propertyName
? `${state.parentNode}.${propertyName} = ${value};` ? `${state.parentNode}.${propertyName} = ${value};`
: `${generator.helper( : `${generator.helper(
method method
)}( ${state.parentNode}, '${name}', ${value} );`; )}( ${state.parentNode}, '${name}', ${value} );`;
block.builders.create.addLine(statement); block.builders.hydrate.addLine(statement);
// special case autofocus. has to be handled in a bit of a weird way // special case autofocus. has to be handled in a bit of a weird way
if (attribute.value === true && name === 'autofocus') { if (attribute.value === true && name === 'autofocus') {
@ -152,7 +156,7 @@ export default function visitAttribute(
if (isIndirectlyBoundValue) { if (isIndirectlyBoundValue) {
const updateValue = `${state.parentNode}.value = ${state.parentNode}.__value;`; const updateValue = `${state.parentNode}.value = ${state.parentNode}.__value;`;
block.builders.create.addLine(updateValue); block.builders.hydrate.addLine(updateValue);
if (isDynamic) block.builders.update.addLine(updateValue); if (isDynamic) block.builders.update.addLine(updateValue);
} }
} }

@ -91,7 +91,7 @@ export default function visitBinding(
`; `;
generator.hasComplexBindings = true; generator.hasComplexBindings = true;
block.builders.create.addBlock( block.builders.hydrate.addBlock(
`if ( !('${name}' in state) ) ${block.component}._bindings.push( ${handler} );` `if ( !('${name}' in state) ) ${block.component}._bindings.push( ${handler} );`
); );
} else if (attribute.name === 'group') { } else if (attribute.name === 'group') {
@ -107,7 +107,7 @@ export default function visitBinding(
? `~${snippet}.indexOf( ${state.parentNode}.__value )` ? `~${snippet}.indexOf( ${state.parentNode}.__value )`
: `${state.parentNode}.__value === ${snippet}`; : `${state.parentNode}.__value === ${snippet}`;
block.builders.create.addLine( block.builders.hydrate.addLine(
`${block.component}._bindingGroups[${bindingGroup}].push( ${state.parentNode} );` `${block.component}._bindingGroups[${bindingGroup}].push( ${state.parentNode} );`
); );
@ -118,7 +118,7 @@ export default function visitBinding(
updateElement = `${state.parentNode}.checked = ${condition};`; updateElement = `${state.parentNode}.checked = ${condition};`;
} else if (node.name === 'audio' || node.name === 'video') { } else if (node.name === 'audio' || node.name === 'video') {
generator.hasComplexBindings = true; generator.hasComplexBindings = true;
block.builders.create.addBlock( block.builders.hydrate.addBlock(
`${block.component}._bindings.push( ${handler} );` `${block.component}._bindings.push( ${handler} );`
); );
@ -144,13 +144,15 @@ export default function visitBinding(
} }
} }
block.builders.create.addBlock(deindent` block.builders.init.addBlock(deindent`
function ${handler} () { function ${handler} () {
${lock} = true; ${lock} = true;
${setter} ${setter}
${lock} = false; ${lock} = false;
} }
`);
block.builders.hydrate.addBlock(deindent`
${generator.helper( ${generator.helper(
'addListener' 'addListener'
)}( ${state.parentNode}, '${eventName}', ${handler} ); )}( ${state.parentNode}, '${eventName}', ${handler} );

@ -6,6 +6,7 @@ import visitAttribute from './Attribute';
import visitEventHandler from './EventHandler'; import visitEventHandler from './EventHandler';
import visitBinding from './Binding'; import visitBinding from './Binding';
import visitRef from './Ref'; import visitRef from './Ref';
import * as namespaces from '../../../../utils/namespaces';
import addTransitions from './addTransitions'; import addTransitions from './addTransitions';
import { DomGenerator } from '../../index'; import { DomGenerator } from '../../index';
import Block from '../../Block'; import Block from '../../Block';
@ -47,18 +48,27 @@ export default function visitElement(
const childState = node._state; const childState = node._state;
const name = childState.parentNode; const name = childState.parentNode;
block.builders.create.addLine( const isToplevel = !state.parentNode;
`var ${name} = ${getRenderStatement(
generator, block.addVariable(name);
childState.namespace, block.builders.create.addLine(`${name} = ${getRenderStatement(generator, childState.namespace, node.name)};`);
node.name
)};` if (generator.hydratable) {
); block.builders.claim.addBlock(deindent`
block.mount(name, state.parentNode); ${name} = ${getClaimStatement(generator, childState.namespace, state.parentNodes, node)};
var ${childState.parentNodes} = ${generator.helper('children')}( ${name} );
`);
}
if (state.parentNode) {
block.builders.mount.addLine(`${block.generator.helper('appendNode')}( ${name}, ${state.parentNode} );`);
} else {
block.builders.mount.addLine(`${block.generator.helper('insertNode')}( ${name}, ${block.target}, anchor );`);
}
// add CSS encapsulation attribute // add CSS encapsulation attribute
if (generator.cssId && (!generator.cascade || state.isTopLevel)) { if (generator.cssId && (!generator.cascade || state.isTopLevel)) {
block.builders.create.addLine( block.builders.hydrate.addLine(
`${generator.helper( `${generator.helper(
'setAttribute' 'setAttribute'
)}( ${name}, '${generator.cssId}', '' );` )}( ${name}, '${generator.cssId}', '' );`
@ -107,7 +117,7 @@ export default function visitElement(
}); });
if (initialProps.length) { if (initialProps.length) {
block.builders.create.addBlock(deindent` block.builders.hydrate.addBlock(deindent`
${name}._svelte = { ${name}._svelte = {
${initialProps.join(',\n')} ${initialProps.join(',\n')}
}; };
@ -120,7 +130,7 @@ export default function visitElement(
} }
} }
if (!state.parentNode) { if (isToplevel) {
// TODO we eventually need to consider what happens to elements // TODO we eventually need to consider what happens to elements
// that belong to the same outgroup as an outroing element... // that belong to the same outgroup as an outroing element...
block.builders.unmount.addLine( block.builders.unmount.addLine(
@ -174,8 +184,12 @@ export default function visitElement(
} }
if (node.initialUpdate) { if (node.initialUpdate) {
block.builders.create.addBlock(node.initialUpdate); block.builders.mount.addBlock(node.initialUpdate);
} }
block.builders.claim.addLine(
`${childState.parentNodes}.forEach( ${generator.helper('detachNode')} );`
);
} }
function getRenderStatement( function getRenderStatement(
@ -193,3 +207,24 @@ function getRenderStatement(
return `${generator.helper('createElement')}( '${name}' )`; return `${generator.helper('createElement')}( '${name}' )`;
} }
function getClaimStatement(
generator: DomGenerator,
namespace: string,
nodes: string,
node: Node
) {
const attributes = node.attributes
.filter((attr: Node) => attr.type === 'Attribute')
.map((attr: Node) => `${quoteProp(attr.name)}: true`)
.join(', ');
const name = namespace ? node.name : node.name.toUpperCase();
return `${generator.helper('claimElement')}( ${nodes}, '${name}', ${attributes ? `{ ${attributes} }` : `{}`}, ${namespace === namespaces.svg ? true : false} )`;
}
function quoteProp(name: string) {
if (/[^a-zA-Z_$0-9]/.test(name)) return `'${name}'`;
return name;
}

@ -68,36 +68,38 @@ export default function visitEventHandler(
[${attribute.expression.start}-${attribute.expression.end}]; [${attribute.expression.start}-${attribute.expression.end}];
`; `;
const handler = isCustomEvent if (isCustomEvent) {
? deindent` block.addVariable(handlerName);
var ${handlerName} = ${generator.alias(
block.builders.hydrate.addBlock(deindent`
${handlerName} = ${generator.alias(
'template' 'template'
)}.events.${name}.call( ${block.component}, ${state.parentNode}, function ( event ) { )}.events.${name}.call( ${block.component}, ${state.parentNode}, function ( event ) {
${handlerBody} ${handlerBody}
}); });
` `);
: deindent`
block.builders.destroy.addLine(deindent`
${handlerName}.teardown();
`);
} else {
const handler = deindent`
function ${handlerName} ( event ) { function ${handlerName} ( event ) {
${handlerBody} ${handlerBody}
} }
`; `;
if (shouldHoist) { if (shouldHoist) {
generator.blocks.push( generator.blocks.push(
<Block>{ <Block>{
render: () => handler, render: () => handler,
} }
); );
} else { } else {
block.builders.create.addBlock(handler); block.builders.init.addBlock(handler);
} }
if (isCustomEvent) { block.builders.hydrate.addLine(deindent`
block.builders.destroy.addLine(deindent`
${handlerName}.teardown();
`);
} else {
block.builders.create.addLine(deindent`
${generator.helper( ${generator.helper(
'addListener' 'addListener'
)}( ${state.parentNode}, '${name}', ${handlerName} ); )}( ${state.parentNode}, '${name}', ${handlerName} );

@ -13,11 +13,11 @@ export default function visitRef(
) { ) {
const name = attribute.name; const name = attribute.name;
block.builders.create.addLine( block.builders.mount.addLine(
`${block.component}.refs.${name} = ${state.parentNode};` `${block.component}.refs.${name} = ${state.parentNode};`
); );
block.builders.destroy.addLine(deindent` block.builders.unmount.addLine(deindent`
if ( ${block.component}.refs.${name} === ${state.parentNode} ) ${block.component}.refs.${name} = null; if ( ${block.component}.refs.${name} === ${state.parentNode} ) ${block.component}.refs.${name} = null;
`); `);

@ -57,7 +57,7 @@ export default function visitWindow(
[${attribute.expression.start}-${attribute.expression.end}]; [${attribute.expression.start}-${attribute.expression.end}];
`; `;
block.builders.create.addBlock(deindent` block.builders.init.addBlock(deindent`
function ${handlerName} ( event ) { function ${handlerName} ( event ) {
${handlerBody} ${handlerBody}
}; };
@ -121,7 +121,7 @@ export default function visitWindow(
${event === 'scroll' && `${lock} = false;`} ${event === 'scroll' && `${lock} = false;`}
`; `;
block.builders.create.addBlock(deindent` block.builders.init.addBlock(deindent`
function ${handlerName} ( event ) { function ${handlerName} ( event ) {
${handlerBody} ${handlerBody}
}; };
@ -137,7 +137,7 @@ export default function visitWindow(
if (bindings.scrollX && bindings.scrollY) { if (bindings.scrollX && bindings.scrollY) {
const observerCallback = block.getUniqueName(`scrollobserver`); const observerCallback = block.getUniqueName(`scrollobserver`);
block.builders.create.addBlock(deindent` block.builders.init.addBlock(deindent`
function ${observerCallback} () { function ${observerCallback} () {
if ( ${lock} ) return; if ( ${lock} ) return;
var x = ${bindings.scrollX var x = ${bindings.scrollX
@ -151,17 +151,17 @@ export default function visitWindow(
`); `);
if (bindings.scrollX) if (bindings.scrollX)
block.builders.create.addLine( block.builders.init.addLine(
`${block.component}.observe( '${bindings.scrollX}', ${observerCallback} );` `${block.component}.observe( '${bindings.scrollX}', ${observerCallback} );`
); );
if (bindings.scrollY) if (bindings.scrollY)
block.builders.create.addLine( block.builders.init.addLine(
`${block.component}.observe( '${bindings.scrollY}', ${observerCallback} );` `${block.component}.observe( '${bindings.scrollY}', ${observerCallback} );`
); );
} else if (bindings.scrollX || bindings.scrollY) { } else if (bindings.scrollX || bindings.scrollY) {
const isX = !!bindings.scrollX; const isX = !!bindings.scrollX;
block.builders.create.addBlock(deindent` block.builders.init.addBlock(deindent`
${block.component}.observe( '${bindings.scrollX || ${block.component}.observe( '${bindings.scrollX ||
bindings.scrollY}', function ( ${isX ? 'x' : 'y'} ) { bindings.scrollY}', function ( ${isX ? 'x' : 'y'} ) {
if ( ${lock} ) return; if ( ${lock} ) return;
@ -173,7 +173,7 @@ export default function visitWindow(
// another special case. (I'm starting to think these are all special cases.) // another special case. (I'm starting to think these are all special cases.)
if (bindings.online) { if (bindings.online) {
const handlerName = block.getUniqueName(`onlinestatuschanged`); const handlerName = block.getUniqueName(`onlinestatuschanged`);
block.builders.create.addBlock(deindent` block.builders.init.addBlock(deindent`
function ${handlerName} ( event ) { function ${handlerName} ( event ) {
${block.component}.set({ ${bindings.online}: navigator.onLine }); ${block.component}.set({ ${bindings.online}: navigator.onLine });
}; };

@ -105,10 +105,19 @@ export default function visitIfBlock(
simple(generator, block, state, node, branches[0], dynamic, vars); simple(generator, block, state, node, branches[0], dynamic, vars);
} }
block.builders.create.addLine(
`${if_name}${name}.create();`
);
block.builders.claim.addLine(
`${if_name}${name}.claim( ${state.parentNodes} );`
);
if (node.needsAnchor) { if (node.needsAnchor) {
block.addElement( block.addElement(
anchor, anchor,
`${generator.helper('createComment')}()`, `${generator.helper('createComment')}()`,
`${generator.helper('createComment')}()`,
state.parentNode, state.parentNode,
true true
); );
@ -126,22 +135,18 @@ function simple(
dynamic, dynamic,
{ name, anchor, params, if_name } { name, anchor, params, if_name }
) { ) {
block.builders.create.addBlock(deindent` block.builders.init.addBlock(deindent`
var ${name} = (${branch.condition}) && ${branch.block}( ${params}, ${block.component} ); var ${name} = (${branch.condition}) && ${branch.block}( ${params}, ${block.component} );
`); `);
const isTopLevel = !state.parentNode; const isTopLevel = !state.parentNode;
const mountOrIntro = branch.hasIntroMethod ? 'intro' : 'mount'; const mountOrIntro = branch.hasIntroMethod ? 'intro' : 'mount';
const targetNode = state.parentNode || block.target;
const anchorNode = state.parentNode ? 'null' : 'anchor';
if (isTopLevel) { block.builders.mount.addLine(
block.builders.mount.addLine( `if ( ${name} ) ${name}.${mountOrIntro}( ${targetNode}, ${anchorNode} );`
`if ( ${name} ) ${name}.${mountOrIntro}( ${block.target}, anchor );` );
);
} else {
block.builders.create.addLine(
`if ( ${name} ) ${name}.${mountOrIntro}( ${state.parentNode}, null );`
);
}
const parentNode = state.parentNode || `${anchor}.parentNode`; const parentNode = state.parentNode || `${anchor}.parentNode`;
@ -152,6 +157,7 @@ function simple(
${name}.update( changed, ${params} ); ${name}.update( changed, ${params} );
} else { } else {
${name} = ${branch.block}( ${params}, ${block.component} ); ${name} = ${branch.block}( ${params}, ${block.component} );
if ( ${name} ) ${name}.create();
} }
${name}.intro( ${parentNode}, ${anchor} ); ${name}.intro( ${parentNode}, ${anchor} );
@ -161,17 +167,20 @@ function simple(
${name}.update( changed, ${params} ); ${name}.update( changed, ${params} );
} else { } else {
${name} = ${branch.block}( ${params}, ${block.component} ); ${name} = ${branch.block}( ${params}, ${block.component} );
${name}.create();
${name}.mount( ${parentNode}, ${anchor} ); ${name}.mount( ${parentNode}, ${anchor} );
} }
` `
: branch.hasIntroMethod : branch.hasIntroMethod
? deindent` ? deindent`
if ( !${name} ) ${name} = ${branch.block}( ${params}, ${block.component} ); if ( !${name} ) ${name} = ${branch.block}( ${params}, ${block.component} );
${name}.create();
${name}.intro( ${parentNode}, ${anchor} ); ${name}.intro( ${parentNode}, ${anchor} );
` `
: deindent` : deindent`
if ( !${name} ) { if ( !${name} ) {
${name} = ${branch.block}( ${params}, ${block.component} ); ${name} = ${branch.block}( ${params}, ${block.component} );
${name}.create();
${name}.mount( ${parentNode}, ${anchor} ); ${name}.mount( ${parentNode}, ${anchor} );
} }
`; `;
@ -218,7 +227,7 @@ function compound(
const current_block = block.getUniqueName(`current_block`); const current_block = block.getUniqueName(`current_block`);
const current_block_and = hasElse ? '' : `${current_block} && `; const current_block_and = hasElse ? '' : `${current_block} && `;
block.builders.create.addBlock(deindent` block.builders.init.addBlock(deindent`
function ${get_block} ( ${params} ) { function ${get_block} ( ${params} ) {
${branches ${branches
.map(({ condition, block }) => { .map(({ condition, block }) => {
@ -234,24 +243,27 @@ function compound(
const isTopLevel = !state.parentNode; const isTopLevel = !state.parentNode;
const mountOrIntro = branches[0].hasIntroMethod ? 'intro' : 'mount'; const mountOrIntro = branches[0].hasIntroMethod ? 'intro' : 'mount';
if (isTopLevel) { const targetNode = state.parentNode || block.target;
block.builders.mount.addLine( const anchorNode = state.parentNode ? 'null' : 'anchor';
`${if_name}${name}.${mountOrIntro}( ${block.target}, anchor );` block.builders.mount.addLine(
); `${if_name}${name}.${mountOrIntro}( ${targetNode}, ${anchorNode} );`
} else { );
block.builders.create.addLine(
`${if_name}${name}.${mountOrIntro}( ${state.parentNode}, null );`
);
}
const parentNode = state.parentNode || `${anchor}.parentNode`; const parentNode = state.parentNode || `${anchor}.parentNode`;
const changeBlock = deindent` const changeBlock = deindent`
${if_name}{ ${hasElse ?
${name}.unmount(); deindent`
${name}.destroy(); ${name}.unmount();
} ${name}.destroy();
` :
deindent`
if ( ${name} ) {
${name}.unmount();
${name}.destroy();
}`}
${name} = ${current_block_and}${current_block}( ${params}, ${block.component} ); ${name} = ${current_block_and}${current_block}( ${params}, ${block.component} );
${if_name}${name}.create();
${if_name}${name}.${mountOrIntro}( ${parentNode}, ${anchor} ); ${if_name}${name}.${mountOrIntro}( ${parentNode}, ${anchor} );
`; `;
@ -304,7 +316,7 @@ function compoundWithOutros(
block.addVariable(current_block_index); block.addVariable(current_block_index);
block.addVariable(name); block.addVariable(name);
block.builders.create.addBlock(deindent` block.builders.init.addBlock(deindent`
var ${if_block_creators} = [ var ${if_block_creators} = [
${branches.map(branch => branch.block).join(',\n')} ${branches.map(branch => branch.block).join(',\n')}
]; ];
@ -323,12 +335,12 @@ function compoundWithOutros(
`); `);
if (hasElse) { if (hasElse) {
block.builders.create.addBlock(deindent` block.builders.init.addBlock(deindent`
${current_block_index} = ${get_block}( ${params} ); ${current_block_index} = ${get_block}( ${params} );
${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, ${block.component} ); ${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, ${block.component} );
`); `);
} else { } else {
block.builders.create.addBlock(deindent` block.builders.init.addBlock(deindent`
if ( ~( ${current_block_index} = ${get_block}( ${params} ) ) ) { if ( ~( ${current_block_index} = ${get_block}( ${params} ) ) ) {
${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, ${block.component} ); ${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, ${block.component} );
} }
@ -337,16 +349,12 @@ function compoundWithOutros(
const isTopLevel = !state.parentNode; const isTopLevel = !state.parentNode;
const mountOrIntro = branches[0].hasIntroMethod ? 'intro' : 'mount'; const mountOrIntro = branches[0].hasIntroMethod ? 'intro' : 'mount';
const targetNode = state.parentNode || block.target;
const anchorNode = state.parentNode ? 'null' : 'anchor';
if (isTopLevel) { block.builders.mount.addLine(
block.builders.mount.addLine( `${if_current_block_index}${if_blocks}[ ${current_block_index} ].${mountOrIntro}( ${targetNode}, ${anchorNode} );`
`${if_current_block_index}${if_blocks}[ ${current_block_index} ].${mountOrIntro}( ${block.target}, anchor );` );
);
} else {
block.builders.create.addLine(
`${if_current_block_index}${if_blocks}[ ${current_block_index} ].${mountOrIntro}( ${state.parentNode}, null );`
);
}
const parentNode = state.parentNode || `${anchor}.parentNode`; const parentNode = state.parentNode || `${anchor}.parentNode`;
@ -360,6 +368,7 @@ function compoundWithOutros(
const createNewBlock = deindent` const createNewBlock = deindent`
${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, ${block.component} ); ${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, ${block.component} );
${name}.create();
${name}.${mountOrIntro}( ${parentNode}, ${anchor} ); ${name}.${mountOrIntro}( ${parentNode}, ${anchor} );
`; `;

@ -19,6 +19,7 @@ export default function visitMustacheTag(
block.addElement( block.addElement(
name, name,
`${generator.helper('createText')}( ${value} = ${snippet} )`, `${generator.helper('createText')}( ${value} = ${snippet} )`,
generator.hydratable ? `${generator.helper('claimText')}( ${state.parentNodes}, ${value} = ${snippet} )` : '',
state.parentNode, state.parentNode,
true true
); );

@ -17,34 +17,33 @@ export default function visitRawMustacheTag(
const { snippet } = block.contextualise(node.expression); const { snippet } = block.contextualise(node.expression);
block.addVariable(value);
// we would have used comments here, but the `insertAdjacentHTML` api only // we would have used comments here, but the `insertAdjacentHTML` api only
// exists for `Element`s. // exists for `Element`s.
block.addElement( block.addElement(
before, before,
`${generator.helper('createElement')}( 'noscript' )`, `${generator.helper('createElement')}( 'noscript' )`,
`${generator.helper('createElement')}( 'noscript' )`,
state.parentNode, state.parentNode,
true true
); );
block.addElement( block.addElement(
after, after,
`${generator.helper('createElement')}( 'noscript' )`, `${generator.helper('createElement')}( 'noscript' )`,
`${generator.helper('createElement')}( 'noscript' )`,
state.parentNode, state.parentNode,
true true
); );
const isToplevel = !state.parentNode; const isToplevel = !state.parentNode;
block.builders.create.addLine(`var ${value} = ${snippet};`); const mountStatement = `${before}.insertAdjacentHTML( 'afterend', ${value} = ${snippet} );`;
const mountStatement = `${before}.insertAdjacentHTML( 'afterend', ${value} );`;
const detachStatement = `${generator.helper( const detachStatement = `${generator.helper(
'detachBetween' 'detachBetween'
)}( ${before}, ${after} );`; )}( ${before}, ${after} );`;
if (isToplevel) { block.builders.mount.addLine(mountStatement);
block.builders.mount.addLine(mountStatement);
} else {
block.builders.create.addLine(mountStatement);
}
block.builders.update.addBlock(deindent` block.builders.update.addBlock(deindent`
if ( ${value} !== ( ${value} = ${snippet} ) ) { if ( ${value} !== ( ${value} = ${snippet} ) ) {

@ -13,6 +13,7 @@ export default function visitText(
block.addElement( block.addElement(
node._state.name, node._state.name,
`${generator.helper('createText')}( ${JSON.stringify(node.data)} )`, `${generator.helper('createText')}( ${JSON.stringify(node.data)} )`,
generator.hydratable ? `${generator.helper('claimText')}( ${state.parentNodes}, ${JSON.stringify(node.data)} )` : '',
state.parentNode, state.parentNode,
node.usedAsAnchor node.usedAsAnchor
); );

@ -9,7 +9,7 @@ export default function visitYieldTag(
) { ) {
const parentNode = state.parentNode || block.target; const parentNode = state.parentNode || block.target;
(state.parentNode ? block.builders.create : block.builders.mount).addLine( block.builders.mount.addLine(
`if ( ${block.component}._yield ) ${block.component}._yield.mount( ${parentNode}, null );` `if ( ${block.component}._yield ) ${block.component}._yield.mount( ${parentNode}, null );`
); );

@ -41,6 +41,7 @@ export interface CompileOptions {
dev?: boolean; dev?: boolean;
shared?: boolean | string; shared?: boolean | string;
cascade?: boolean; cascade?: boolean;
hydratable?: boolean;
onerror?: (error: Error) => void; onerror?: (error: Error) => void;
onwarn?: (warning: Warning) => void; onwarn?: (warning: Warning) => void;

@ -66,3 +66,34 @@ export function getBindingGroupValue(group) {
export function toNumber(value) { export function toNumber(value) {
return value === '' ? undefined : +value; return value === '' ? undefined : +value;
} }
export function children (element) {
return Array.from(element.childNodes);
}
export function claimElement (nodes, name, attributes, svg) {
for (var i = 0; i < nodes.length; i += 1) {
var node = nodes[i];
if (node.nodeName === name) {
for (var j = 0; j < node.attributes.length; j += 1) {
var attribute = node.attributes[j];
if (!attributes[attribute.name]) node.removeAttribute(attribute.name);
}
return nodes.splice(i, 1)[0]; // TODO strip unwanted attributes
}
}
return svg ? createSvgElement(name) : createElement(name);
}
export function claimText (nodes, data) {
for (var i = 0; i < nodes.length; i += 1) {
var node = nodes[i];
if (node.nodeType === 3) {
node.data = data;
return nodes.splice(i, 1)[0];
}
}
return createText(data);
}

@ -167,8 +167,9 @@ function capitalize(str) {
return str[0].toUpperCase() + str.slice(1); return str[0].toUpperCase() + str.slice(1);
} }
export function showOutput(cwd, options) { export function showOutput(cwd, options = {}) {
glob.sync('**/*.html', { cwd }).forEach(file => { glob.sync('**/*.html', { cwd }).forEach(file => {
if (file[0] === '_') return;
const { code } = svelte.compile( const { code } = svelte.compile(
fs.readFileSync(`${cwd}/${file}`, 'utf-8'), fs.readFileSync(`${cwd}/${file}`, 'utf-8'),
Object.assign(options, { Object.assign(options, {

@ -0,0 +1,105 @@
import assert from 'assert';
import path from 'path';
import fs from 'fs';
import {
showOutput,
loadConfig,
loadSvelte,
env,
setupHtmlEqual
} from '../helpers.js';
let compileOptions = null;
function getName(filename) {
const base = path.basename(filename).replace('.html', '');
return base[0].toUpperCase() + base.slice(1);
}
const nodeVersionMatch = /^v(\d)/.exec(process.version);
const legacy = +nodeVersionMatch[1] < 6;
const babelrc = require('../../package.json').babel;
describe('hydration', () => {
before(() => {
const svelte = loadSvelte();
require.extensions['.html'] = function(module, filename) {
const options = Object.assign(
{ filename, name: getName(filename), hydratable: true },
compileOptions
);
let { code } = svelte.compile(fs.readFileSync(filename, 'utf-8'), options);
if (legacy) code = require('babel-core').transform(code, babelrc).code;
return module._compile(code, filename);
};
return setupHtmlEqual();
});
function runTest(dir) {
if (dir[0] === '.') return;
const config = loadConfig(`./hydration/samples/${dir}/_config.js`);
if (config.solo && process.env.CI) {
throw new Error('Forgot to remove `solo: true` from test');
}
(config.skip ? it.skip : config.solo ? it.only : it)(dir, () => {
const cwd = path.resolve(`test/hydration/samples/${dir}`);
compileOptions = config.compileOptions || {};
compileOptions.shared = path.resolve('shared.js');
compileOptions.dev = config.dev;
compileOptions.hydrate = true;
return env()
.then(window => {
global.window = window;
let SvelteComponent;
try {
SvelteComponent = require(`${cwd}/main.html`).default;
} catch (err) {
throw err;
}
const target = window.document.body;
target.innerHTML = fs.readFileSync(`${cwd}/_before.html`, 'utf-8');
const snapshot = config.snapshot ? config.snapshot(target) : {};
const component = new SvelteComponent({
target,
hydrate: true,
data: config.data
});
assert.htmlEqual(target.innerHTML, fs.readFileSync(`${cwd}/_after.html`, 'utf-8'));
if (config.test) {
config.test(assert, target, snapshot, component, window);
} else {
component.destroy();
assert.equal(target.innerHTML, '');
}
})
.catch(err => {
showOutput(cwd, { shared: 'svelte/shared.js' }); // eslint-disable-line no-console
throw err;
})
.then(() => {
if (config.show) showOutput(cwd, { shared: 'svelte/shared.js' });
});
});
}
fs.readdirSync('test/hydration/samples').forEach(dir => {
runTest(dir, null);
});
});

@ -0,0 +1,17 @@
export default {
snapshot(target) {
const h1 = target.querySelector('h1');
return {
h1,
text: h1.childNodes[0]
};
},
test(assert, target, snapshot) {
const h1 = target.querySelector('h1');
assert.equal(h1, snapshot.h1);
assert.equal(h1.childNodes[0], snapshot.text);
}
};

@ -0,0 +1,2 @@
<input>
<p>Hello world!</p>

@ -0,0 +1,2 @@
<input>
<p>Hello world!</p>

@ -0,0 +1,29 @@
export default {
data: {
name: 'world'
},
snapshot(target) {
return {
input: target.querySelector('input'),
p: target.querySelector('p')
};
},
test(assert, target, snapshot, component, window) {
const input = target.querySelector('input');
const p = target.querySelector('p');
assert.equal(input, snapshot.input);
assert.equal(p, snapshot.p);
input.value = 'everybody';
input.dispatchEvent(new window.Event('input'));
assert.equal(component.get('name'), 'everybody');
assert.htmlEqual(target.innerHTML, `
<input>
<p>Hello everybody!</p>
`);
}
};

@ -0,0 +1,2 @@
<input bind:value='name'>
<p>Hello {{name}}!</p>

@ -0,0 +1,21 @@
export default {
snapshot(target) {
const div = target.querySelector('div');
const p = target.querySelector('p');
return {
div,
p,
text: p.childNodes[0]
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
const p = target.querySelector('p');
assert.equal(div, snapshot.div);
assert.equal(p, snapshot.p);
assert.equal(p.childNodes[0], snapshot.text);
}
};

@ -0,0 +1,13 @@
<div>
<Nested/>
</div>
<script>
import Nested from './Nested.html';
export default {
components: {
Nested
}
};
</script>

@ -0,0 +1,17 @@
export default {
snapshot(target) {
const p = target.querySelector('p');
return {
p,
text: p.childNodes[0]
};
},
test(assert, target, snapshot) {
const p = target.querySelector('p');
assert.equal(p, snapshot.p);
assert.equal(p.childNodes[0], snapshot.text);
}
};

@ -0,0 +1,11 @@
<Nested/>
<script>
import Nested from './Nested.html';
export default {
components: {
Nested
}
};
</script>

@ -0,0 +1,21 @@
export default {
data: {
name: 'everybody'
},
snapshot(target) {
const h1 = target.querySelector('h1');
return {
h1,
text: h1.childNodes[0]
};
},
test(assert, target, snapshot) {
const h1 = target.querySelector('h1');
assert.equal(h1, snapshot.h1);
assert.equal(h1.childNodes[0], snapshot.text);
}
};

@ -0,0 +1,21 @@
export default {
data: {
name: 'world'
},
snapshot(target) {
const h1 = target.querySelector('h1');
return {
h1,
text: h1.childNodes[0]
};
},
test(assert, target, snapshot) {
const h1 = target.querySelector('h1');
assert.equal(h1, snapshot.h1);
assert.equal(h1.childNodes[0], snapshot.text);
}
};

@ -0,0 +1,5 @@
<ul>
<li>animal</li>
<li>vegetable</li>
<li>mineral</li>
</ul>

@ -0,0 +1,5 @@
<ul>
<li>animal</li>
<li>vegetable</li>
<li>mineral</li>
</ul>

@ -0,0 +1,29 @@
export default {
data: {
things: [
'animal',
'vegetable',
'mineral'
]
},
snapshot(target) {
const ul = target.querySelector('ul');
const lis = ul.querySelectorAll('li');
return {
ul,
lis
};
},
test(assert, target, snapshot) {
const ul = target.querySelector('ul');
const lis = ul.querySelectorAll('li');
assert.equal(ul, snapshot.ul);
assert.equal(lis[0], snapshot.lis[0]);
assert.equal(lis[1], snapshot.lis[1]);
assert.equal(lis[2], snapshot.lis[2]);
}
};

@ -0,0 +1,5 @@
<ul>
{{#each things as thing}}
<li>{{thing}}</li>
{{/each}}
</ul>

@ -0,0 +1,19 @@
export default {
data: {
class: 'bar'
},
snapshot(target) {
const div = target.querySelector('div');
return {
div
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
}
};

@ -0,0 +1,19 @@
export default {
data: {
class: 'bar'
},
snapshot(target) {
const div = target.querySelector('div');
return {
div
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
}
};

@ -0,0 +1,19 @@
export default {
data: {
class: 'bar'
},
snapshot(target) {
const div = target.querySelector('div');
return {
div
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
}
};

@ -0,0 +1,15 @@
export default {
snapshot(target) {
const div = target.querySelector('div');
return {
div
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
}
};

@ -0,0 +1,17 @@
export default {
snapshot(target) {
const div = target.querySelector('div');
return {
div,
p: div.querySelector('p')
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
assert.equal(div.querySelector('p'), snapshot.p);
}
};

@ -0,0 +1,3 @@
<div>
<p>nested</p>
</div>

@ -0,0 +1,16 @@
export default {
snapshot(target) {
const h1 = target.querySelector('h1');
return {
h1,
};
},
test(assert, target, snapshot, component) {
const h1 = target.querySelector('h1');
assert.equal(h1, snapshot.h1);
assert.equal(component.refs.h1, h1);
}
};

@ -0,0 +1 @@
<h1 ref:h1>Hello world!</h1>

@ -0,0 +1,26 @@
export default {
data: {
clicked: false
},
snapshot(target) {
const button = target.querySelector('button');
return {
button
};
},
test(assert, target, snapshot, component, window) {
const button = target.querySelector('button');
assert.equal(button, snapshot.button);
button.dispatchEvent(new window.MouseEvent('click'));
assert.ok(component.get('clicked'));
assert.htmlEqual(target.innerHTML, `
<button>click me</button>
<p>clicked!</p>
`);
}
};

@ -0,0 +1,5 @@
<button on:click='set({ clicked: true })'>click me</button>
{{#if clicked}}
<p>clicked!</p>
{{/if}}

@ -0,0 +1,4 @@
<div>
<p>foo!</p>
<p>bar!</p>
</div>

@ -0,0 +1,4 @@
<div>
<p>foo!</p>
<p>bar!</p>
</div>

@ -0,0 +1,26 @@
export default {
data: {
foo: true,
bar: true
},
snapshot(target) {
const div = target.querySelector('div');
const ps = target.querySelectorAll('p');
return {
div,
p0: ps[0],
p1: ps[1]
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
const ps = target.querySelectorAll('p');
assert.equal(div, snapshot.div);
assert.equal(ps[0], snapshot.p0);
assert.equal(ps[1], snapshot.p1);
}
};

@ -0,0 +1,9 @@
<div>
{{#if foo}}
<p>foo!</p>
{{/if}}
{{#if bar}}
<p>bar!</p>
{{/if}}
</div>

@ -0,0 +1,19 @@
export default {
data: {
foo: false
},
snapshot(target) {
const p = target.querySelector('p');
return {
p
};
},
test(assert, target, snapshot) {
const p = target.querySelector('p');
assert.equal(p, snapshot.p);
}
};

@ -0,0 +1,5 @@
<p>before</p>
{{#if foo}}
<p>foo!</p>
{{/if}}
<p>after</p>

@ -0,0 +1,23 @@
export default {
data: {
foo: true,
bar: false
},
snapshot(target) {
const p = target.querySelector('p');
return {
p
};
},
test(assert, target, snapshot, component) {
const p = target.querySelector('p');
assert.equal(p, snapshot.p);
component.set({ foo: false, bar: true });
assert.htmlEqual(target.innerHTML, `<p>bar!</p>`);
}
};

@ -0,0 +1,7 @@
{{#if foo}}
<p>foo!</p>
{{/if}}
{{#if bar}}
<p>bar!</p>
{{/if}}

@ -0,0 +1,19 @@
export default {
data: {
foo: true
},
snapshot(target) {
const p = target.querySelector('p');
return {
p
};
},
test(assert, target, snapshot) {
const p = target.querySelector('p');
assert.equal(p, snapshot.p);
}
};

@ -0,0 +1,3 @@
{{#if foo}}
<p>foo!</p>
{{/if}}

@ -0,0 +1,4 @@
<noscript></noscript>
<p>this is some html</p>
<p>and so is this</p>
<noscript></noscript>

@ -0,0 +1,2 @@
<p>this is some html</p>
<p>and so is this</p>

@ -0,0 +1,27 @@
export default {
skip: true, // existing nodes are blown away
data: {
raw: `<p>this is some html</p> <p>and so is this</p>`
},
snapshot(target) {
const ps = target.querySelectorAll('p');
return {
p0: ps[0],
text0: ps[0].firstChild,
p1: ps[1],
text1: ps[1].firstChild
};
},
test(assert, target, snapshot) {
const ps = target.querySelectorAll('p');
assert.equal(ps[0], snapshot.p0);
assert.equal(ps[0].firstChild, snapshot.text0);
assert.equal(ps[1], snapshot.p1);
assert.equal(ps[1].firstChild, snapshot.text1);
}
};

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save